From 0a349451cd8c346db7b3e71bc31f25c0cdcd56ee Mon Sep 17 00:00:00 2001 From: Robin Sue Date: Tue, 26 Nov 2024 09:19:16 +0100 Subject: [PATCH] Fix Serialization of generic types including collections (#6148) #5682 broke serialization in two ways: - When a generic type was being serialized, such as List`1, the ` was being escaped by JsonSerializer.Serialize to "\u0060". The PolymorphicObjectConverter would then .Trim('"') without unescaping the `, causing "\\u0060" to be written into the final json. - When the type is generic with one type parameter, e.g. List, and also IEnumerable, as List is, then it would append "[]" at the end of the type name, resulting in: "System.Collections.Generic.List\\u00601[[Elsa.Workflows.IntegrationTests.Serialization.JsonSerialization.WorkflowAction, Elsa.Workflows.IntegrationTests]], System.Private.CoreLib[]" which is very invalid. It may only do that, if the typename was simplified because the elementType is in the WellKnownTypeRegistry. And even then, this is questionable, as you'd be deserializing an string[] where you've serialized a List for example. --- .../Converters/PolymorphicObjectConverter.cs | 9 ++---- .../Converters/TypeJsonConverter.cs | 7 ++-- .../Serialization/JsonSerialization/Tests.cs | 32 +++++++++++++++++-- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs index 424daea15..1f3ccc9cb 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs @@ -271,13 +271,10 @@ public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegi { if (shouldWriteTypeField) { - var typeOptions = newOptions.Clone(); - typeOptions.Converters.RemoveWhere(c => c.GetType() != typeof(TypeJsonConverter)); - - if (typeOptions.Converters.Any()) + if (newOptions.Converters.OfType().FirstOrDefault() is { } typeJsonConverter) { - var typeValue = JsonSerializer.Serialize(type, typeOptions).Trim('"'); - writer.WriteString(TypePropertyName, typeValue); + writer.WritePropertyName(TypePropertyName); + typeJsonConverter.Write(writer, type, newOptions); } else { diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs index dee636927..e1dd57cd7 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs @@ -51,15 +51,14 @@ public class TypeJsonConverter : JsonConverter var elementType = value.GenericTypeArguments.First(); var typedEnumerable = typeof(IEnumerable<>).MakeGenericType(elementType); - if (typedEnumerable.IsAssignableFrom(value)) + if (typedEnumerable.IsAssignableFrom(value) && _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementTypeAlias)) { - var elementTypeAlias = _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementAlias) ? elementAlias : value.GetSimpleAssemblyQualifiedName(); - JsonSerializer.Serialize(writer, $"{elementTypeAlias}[]", options); + writer.WriteStringValue($"{elementTypeAlias}[]"); return; } } var typeAlias = _wellKnownTypeRegistry.TryGetAlias(value, out var @alias) ? alias : value.GetSimpleAssemblyQualifiedName(); - JsonSerializer.Serialize(writer, typeAlias, options); + writer.WriteStringValue(typeAlias); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs index 9ce2da7ef..95329ed2c 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs @@ -69,6 +69,26 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) CompareJsonsObjects(expected, result); } + [Fact] + public void RoundtripComplexEnumerableObject() + { + var dict = new Dictionary + { + { "Content", new List() + { + new() + { + Data = "Hello World" + } + } + } + }; + var jsonSerialized = SerializeUsingPayloadSerializer(dict); + var transformationModel = DeSerializeDictionaryUsingPayloadSerializer(jsonSerialized); + var result = transformationModel["Content"]; + Assert.Equal(typeof(List), result.GetType()); + } + private string SerializeUsingPayloadSerializer(object obj) { var payloadSerializer = _services.GetRequiredService(); @@ -93,12 +113,12 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) { var isArray = type == typeof(JsonArray) || type == typeof(JArray); - var jsonContent = isArray ? "[{\"path\":\"folder1\",\"command\":\"add\"}]": "{\"file1\":{\"script\":[{\"path\":\"folder1\",\"command\":\"add\"}]} }"; + var jsonContent = isArray ? "[{\"path\":\"folder1\",\"command\":\"add\"}]" : "{\"file1\":{\"script\":[{\"path\":\"folder1\",\"command\":\"add\"}]} }"; var dict = new Dictionary { { "StatusCode", "Created" }, - { "Content",isArray ? + { "Content",isArray ? (type == typeof(JArray)? JArray.Parse(jsonContent):JsonArray.Parse(jsonContent)): (type == typeof(JObject)? JObject.Parse(jsonContent):JsonObject.Parse(jsonContent)) } @@ -108,7 +128,8 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) private string GetExpected(Type type) { - if (type == typeof(JsonArray) || type == typeof(JArray)) { + if (type == typeof(JsonArray) || type == typeof(JArray)) + { return "[{\"path\":\"folder1\",\"command\":\"add\"}]"; } else @@ -118,4 +139,9 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) } private static string? NormalizeNewlines(string? input) => input?.Replace("\r\n", "\n").Replace("\\r\\n", "\\n"); +} + +public class TestObject +{ + public string? Data { get; set; } } \ No newline at end of file