diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs
index 1bc10a7674..bb9189805c 100644
--- a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs
+++ b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs
@@ -37,13 +37,31 @@ public ModelSerializationService(IAppModel appModel, Telemetry? telemetry = null
///
/// The binary data
/// The data type used to get content type and the classRef for the object to be returned
+ ///
/// The model specified in
- public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType)
+ public object DeserializeFromStorage(
+ ReadOnlySpan data,
+ DataType dataType,
+ string? contentType = "application/xml"
+ )
{
+ contentType ??= "application/xml";
+
+ if (dataType.AllowedContentTypes?.Contains(contentType) == false)
+ {
+ throw new InvalidOperationException(
+ $"Content type {contentType} is not allowed for data type {dataType.Id}"
+ );
+ }
+
var type = GetModelTypeForDataType(dataType);
- // TODO: support sending json to storage based on dataType.ContentTypes
- return DeserializeXml(data, type);
+ return contentType switch
+ {
+ "application/xml" => DeserializeXml(data, type),
+ "application/json" => DeserializeJson(data, type),
+ _ => throw new InvalidOperationException($"Unsupported content type {contentType}"),
+ };
}
///
@@ -51,7 +69,7 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType)
///
/// The object to serialize (must match the classRef in DataType)
/// The data type
- /// the binary data and the content type (currently only application/xml, but likely also json in the future)
+ /// the binary data and the content type (application/xml or application/json)
/// If the classRef in dataType does not match type of the model
public (ReadOnlyMemory data, string contentType) SerializeToStorage(object model, DataType dataType)
{
@@ -63,8 +81,14 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType)
);
}
- //TODO: support sending json to storage based on dataType.ContentTypes
- return (SerializeToXml(model), "application/xml");
+ var contentType = dataType.AllowedContentTypes?.FirstOrDefault() ?? "application/xml";
+
+ return contentType switch
+ {
+ "application/xml" => (SerializeToXml(model), contentType),
+ "application/json" => (SerializeToJson(model), contentType),
+ _ => throw new InvalidOperationException($"Unsupported content type {contentType}"),
+ };
}
///
diff --git a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs
index 2d6503da8d..76b9d0dffc 100644
--- a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs
+++ b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs
@@ -103,7 +103,8 @@ public async Task GetFormData(DataElementIdentifier dataElementIdentifie
return _modelSerializationService.DeserializeFromStorage(
binaryData.Span,
- this.GetDataType(dataElementIdentifier)
+ this.GetDataType(dataElementIdentifier),
+ GetDataElement(dataElementIdentifier).ContentType
);
}
);
@@ -359,7 +360,8 @@ out ReadOnlyMemory cachedBinary
// and deserializing twice is not a big deal
PreviousFormData = _modelSerializationService.DeserializeFromStorage(
cachedBinary.Span,
- dataType
+ dataType,
+ dataElement.ContentType
),
CurrentBinaryData = currentBinary,
PreviousBinaryData = cachedBinary,
diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs
index aaf8378533..da01bb3a39 100644
--- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs
+++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs
@@ -204,7 +204,7 @@ await File.ReadAllBytesAsync(dataElementPath, cancellationToken),
string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId);
var dataBytes = await File.ReadAllBytesAsync(dataPath, cancellationToken);
- var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType);
+ var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType, dataElement?.ContentType);
return formData ?? throw new Exception("Unable to deserialize form data");
}
diff --git a/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs
new file mode 100644
index 0000000000..2d88793fc0
--- /dev/null
+++ b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs
@@ -0,0 +1,228 @@
+using Altinn.App.Core.Helpers.Serialization;
+using Altinn.App.Core.Internal.AppModel;
+using Altinn.Platform.Storage.Interface.Models;
+using FluentAssertions;
+using Moq;
+
+namespace Altinn.App.Core.Tests.Models;
+
+public class ModelSerializationServiceTests
+{
+ private static readonly string _testClassRef = typeof(TestDataModel).AssemblyQualifiedName!;
+ private static readonly string _otherClassRef = typeof(SomeOtherType).AssemblyQualifiedName!;
+
+ private readonly ModelSerializationService _sut;
+
+ private readonly Mock _appModelMock = new Mock();
+
+ public ModelSerializationServiceTests()
+ {
+ _appModelMock.Setup(x => x.GetModelType(It.Is(s => s == _testClassRef))).Returns(typeof(TestDataModel));
+ _appModelMock
+ .Setup(x => x.GetModelType(It.Is(s => s == _otherClassRef)))
+ .Returns(typeof(SomeOtherType));
+
+ _sut = new ModelSerializationService(_appModelMock.Object);
+ }
+
+ [Theory]
+ [InlineData("application/json", "application/json")]
+ [InlineData("application/xml", "application/xml")]
+ [InlineData(null, "application/xml")]
+ public void SerializeToStorage_ReturnsExpectedContentType(string? contentType, string expectedOutputType)
+ {
+ List allowedContentTypes = contentType != null ? [contentType] : [];
+
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var dataType = CreateDataType(allowedContentTypes);
+
+ // Act
+ var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType);
+
+ // Assert
+ outputContentType.Should().Be(expectedOutputType);
+ }
+
+ [Fact]
+ public void SerializeToStorage_SerializesJson()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var dataType = CreateDataType(["application/json"]);
+
+ // Act
+ var (data, _) = _sut.SerializeToStorage(testObject, dataType);
+
+ // Assert
+ var json = System.Text.Encoding.UTF8.GetString(data.Span);
+
+ json.Should().Be("{\"name\":\"Test\",\"value\":42}");
+ }
+
+ [Fact]
+ public void SerializeToStorage_SerializesXml()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var dataType = CreateDataType(["application/xml"]);
+
+ // Act
+ var (data, _) = _sut.SerializeToStorage(testObject, dataType);
+
+ // Assert
+ var xml = System.Text.Encoding.UTF8.GetString(data.Span);
+
+ xml.Should()
+ .Be(
+ "Test 42 "
+ );
+ }
+
+ [Fact]
+ public void SerializeToStorage_ThrowsOnUnsupportedContentType()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var dataType = CreateDataType(["application/unsupported"]);
+
+ // Act
+ var act = () => _sut.SerializeToStorage(testObject, dataType);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void SerializeToStorage_ThrowsOnMismatchingModelType()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var mismatchingDataType = new DataType()
+ {
+ Id = "mismatching",
+ AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef },
+ AllowedContentTypes = ["application/json"],
+ };
+
+ // Act
+ var act = () => _sut.SerializeToStorage(testObject, mismatchingDataType);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void SerializeToStorage_MultipleAllowedContentTypes_PicksTheFirstOne()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+
+ var dataType = CreateDataType(["application/json", "application/xml"]);
+
+ // Act
+ var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType);
+
+ // Assert
+ outputContentType.Should().Be("application/json");
+ }
+
+ [Fact]
+ public void DeserializeFromStorage_ThrowsOnMismatchingModelType()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+ var data = _sut.SerializeToJson(testObject);
+
+ var mismatchingDataType = new DataType()
+ {
+ Id = "mismatching",
+ AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef },
+ };
+
+ // Act
+ var act = () => _sut.DeserializeFromStorage(data.Span, mismatchingDataType);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void DeserializeFromStorage_DeserializesJson()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+ var data = _sut.SerializeToJson(testObject);
+
+ var dataType = CreateDataType(["application/json"]);
+
+ // Act
+ var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/json");
+
+ // Assert
+ result.Should().BeOfType();
+ var model = (TestDataModel)result;
+ model.Name.Should().Be("Test");
+ model.Value.Should().Be(42);
+ }
+
+ [Fact]
+ public void DeserializeFromStorage_DeserializesXml()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+ var data = _sut.SerializeToXml(testObject);
+
+ var dataType = CreateDataType(["application/xml"]);
+
+ // Act
+ var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/xml");
+
+ // Assert
+ result.Should().BeOfType();
+ var model = (TestDataModel)result;
+ model.Name.Should().Be("Test");
+ model.Value.Should().Be(42);
+ }
+
+ [Fact]
+ public void DeserializeFromStorage_WithDefaultContentType_DeserializesXml()
+ {
+ // Arrange
+ var testObject = new TestDataModel { Name = "Test", Value = 42 };
+ var data = _sut.SerializeToXml(testObject);
+
+ var dataType = CreateDataType(["application/xml"]);
+
+ // Act
+ var result = _sut.DeserializeFromStorage(data.Span, dataType);
+
+ // Assert
+ result.Should().BeOfType();
+ var model = (TestDataModel)result;
+ model.Name.Should().Be("Test");
+ model.Value.Should().Be(42);
+ }
+
+ private static DataType CreateDataType(List allowedContentTypes)
+ {
+ return new DataType()
+ {
+ AppLogic = new ApplicationLogic() { ClassRef = _testClassRef },
+ AllowedContentTypes = allowedContentTypes,
+ };
+ }
+
+ public record SomeOtherType { }
+
+ public record TestDataModel
+ {
+ public string Name { get; set; } = string.Empty;
+ public int Value { get; set; }
+ }
+}
diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
index 8e71e4f24f..45e3ddbaf6 100644
--- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
+++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
@@ -2229,7 +2229,7 @@ namespace Altinn.App.Core.Helpers.Serialization
public class ModelSerializationService
{
public ModelSerializationService(Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Features.Telemetry? telemetry = null) { }
- public object DeserializeFromStorage(System.ReadOnlySpan data, Altinn.Platform.Storage.Interface.Models.DataType dataType) { }
+ public object DeserializeFromStorage(System.ReadOnlySpan data, Altinn.Platform.Storage.Interface.Models.DataType dataType, string? contentType = "application/xml") { }
public object DeserializeJson(System.ReadOnlySpan data, System.Type modelType) { }
public System.Threading.Tasks.Task> DeserializeSingleFromStream(System.IO.Stream body, string? contentType, Altinn.Platform.Storage.Interface.Models.DataType dataType) { }
public object DeserializeXml(System.ReadOnlySpan data, System.Type modelType) { }