-
Notifications
You must be signed in to change notification settings - Fork 53
Support complex type deserialization (standalone) #557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| foreach (JsonConverter converter in options.Converters) | ||
| { | ||
| if (converter is ObjectWithTypeMetadataJsonConverterFactory) | ||
| { | ||
| hasConverter = true; | ||
| break; | ||
| } | ||
| } |
| if (wrappedType != null && targetType.IsArray && valueElement.ValueKind == JsonValueKind.Array) | ||
| { | ||
| // Check if wrapped type is a generic collection that implements IEnumerable<T> | ||
| if (wrappedType.IsGenericType) | ||
| { | ||
| Type genericTypeDef = wrappedType.GetGenericTypeDefinition(); | ||
| if (genericTypeDef == typeof(List<>) || | ||
| genericTypeDef == typeof(IList<>) || | ||
| genericTypeDef == typeof(ICollection<>) || | ||
| genericTypeDef == typeof(IEnumerable<>)) | ||
| { | ||
| Type[] genericArgs = wrappedType.GetGenericArguments(); | ||
| if (genericArgs.Length == 1) | ||
| { | ||
| Type elementType = genericArgs[0]; | ||
| Type targetElementType = targetType.GetElementType()!; | ||
|
|
||
| // If the element types match, we can deserialize directly | ||
| if (elementType == targetElementType) | ||
| { | ||
| string unwrappedJson = valueElement.GetRawText(); | ||
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| if (wrappedType != null && wrappedType.IsArray && !targetType.IsArray) | ||
| { | ||
| if (valueElement.ValueKind == JsonValueKind.Array && valueElement.GetArrayLength() > 0) | ||
| { | ||
| // Get the first element of the array | ||
| JsonElement firstElement = valueElement[0]; | ||
| string firstElementJson = firstElement.GetRawText(); | ||
|
|
||
| // Check if the first element is also wrapped | ||
| using (JsonDocument innerDoc = JsonDocument.Parse(firstElementJson)) | ||
| { | ||
| JsonElement innerRoot = innerDoc.RootElement; | ||
| if (innerRoot.ValueKind == JsonValueKind.Object && | ||
| innerRoot.TryGetProperty("$type", out JsonElement innerTypeElement) && | ||
| innerRoot.TryGetProperty("$value", out JsonElement innerValueElement)) | ||
| { | ||
| string innerTypeName = innerTypeElement.GetString()!; | ||
| Type? innerWrappedType = Type.GetType(innerTypeName, throwOnError: false); | ||
|
|
||
| if (innerWrappedType != null && innerWrappedType == targetType) | ||
| { | ||
| // Unwrap the inner object | ||
| string unwrappedJson = innerValueElement.GetRawText(); | ||
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // First element is not wrapped, try to deserialize it directly | ||
| // This handles cases where the array contains a single unwrapped element | ||
| try | ||
| { | ||
| return JsonSerializer.Deserialize(firstElementJson, targetType, this.options); | ||
| } | ||
| catch | ||
| { | ||
| // If deserialization fails, fall through to normal deserialization | ||
| } | ||
| } | ||
| } |
| catch | ||
| { | ||
| // If deserialization fails, fall through to normal deserialization | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces complex type deserialization support for the standalone Durable Task SDK by implementing a type metadata preservation mechanism. The changes fix issue #430 where Dictionary<string, object> values and object-typed properties were being deserialized as JsonElement instead of their original types.
Key Changes:
- Added ObjectWithTypeMetadataJsonConverter that wraps complex types with $type and $value metadata during serialization
- Modified JsonDataConverter to automatically include this converter in default options and handle unwrapping during deserialization
- Added comprehensive test coverage for various serialization scenarios including nested objects, arrays, and primitives
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| src/Abstractions/Converters/ObjectWithTypeMetadataJsonConverter.cs | New converter factory that preserves type information by wrapping complex objects with $type and $value metadata during serialization |
| src/Abstractions/Converters/JsonDataConverter.cs | Modified to include ObjectWithTypeMetadataJsonConverter in default options and added unwrapping logic in Deserialize method |
| test/Abstractions.Tests/Converters/JsonDataConverterTests.cs | New test file with comprehensive coverage for the type preservation feature including dictionaries, arrays, nested objects, and edge cases |
| if (targetType == typeof(JsonElement)) | ||
| { | ||
| using (JsonDocument doc = JsonDocument.Parse(data)) | ||
| { | ||
| JsonElement root = doc.RootElement; | ||
| if (root.ValueKind == JsonValueKind.Object && | ||
| root.TryGetProperty("$type", out JsonElement typeElement) && | ||
| root.TryGetProperty("$value", out JsonElement valueElement)) | ||
| { | ||
| // Unwrap and return the actual value as JsonElement | ||
| return valueElement.Clone(); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JsonDocument is being parsed twice in the Deserialize method - once here for JsonElement unwrapping (lines 88-101) and again immediately after (lines 105-206) for general unwrapping logic. This creates unnecessary overhead as JsonDocument.Parse is called twice on the same data. Consider consolidating these checks into a single parse operation to improve performance.
| innerRoot.TryGetProperty("$value", out JsonElement innerValueElement)) | ||
| { | ||
| string innerTypeName = innerTypeElement.GetString()!; | ||
| Type? innerWrappedType = Type.GetType(innerTypeName, throwOnError: false); |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Type.GetType with type names from JSON input can pose security risks. An attacker could provide a malicious type name that loads unexpected assemblies or triggers unintended code execution during type loading. Consider implementing a type allowlist or validating that loaded types are from trusted assemblies only.
| catch | ||
| { | ||
| // If deserialization fails, fall through to normal deserialization | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty catch block silently swallows all exceptions without logging or handling them. This makes debugging difficult and can hide important errors. At minimum, the exception should be logged, or the catch should be more specific about which exceptions are expected (e.g., JsonException for malformed JSON).
| // Special case: If wrapped type is an array and target type is NOT an array, | ||
| // check if the array contains a single wrapped element that matches the target type | ||
| if (wrappedType != null && wrappedType.IsArray && !targetType.IsArray) | ||
| { | ||
| if (valueElement.ValueKind == JsonValueKind.Array && valueElement.GetArrayLength() > 0) | ||
| { | ||
| // Get the first element of the array | ||
| JsonElement firstElement = valueElement[0]; | ||
| string firstElementJson = firstElement.GetRawText(); | ||
|
|
||
| // Check if the first element is also wrapped | ||
| using (JsonDocument innerDoc = JsonDocument.Parse(firstElementJson)) | ||
| { | ||
| JsonElement innerRoot = innerDoc.RootElement; | ||
| if (innerRoot.ValueKind == JsonValueKind.Object && | ||
| innerRoot.TryGetProperty("$type", out JsonElement innerTypeElement) && | ||
| innerRoot.TryGetProperty("$value", out JsonElement innerValueElement)) | ||
| { | ||
| string innerTypeName = innerTypeElement.GetString()!; | ||
| Type? innerWrappedType = Type.GetType(innerTypeName, throwOnError: false); | ||
|
|
||
| if (innerWrappedType != null && innerWrappedType == targetType) | ||
| { | ||
| // Unwrap the inner object | ||
| string unwrappedJson = innerValueElement.GetRawText(); | ||
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // First element is not wrapped, try to deserialize it directly | ||
| // This handles cases where the array contains a single unwrapped element | ||
| try | ||
| { | ||
| return JsonSerializer.Deserialize(firstElementJson, targetType, this.options); | ||
| } | ||
| catch | ||
| { | ||
| // If deserialization fails, fall through to normal deserialization | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for checking if wrapped type is an array and target is NOT an array (lines 163-204) is complex and appears to be attempting to handle a very specific edge case. The comment mentions "if the array contains a single wrapped element" but the code continues even when the array has multiple elements (only checking the first). This logic seems overly complex for the value it provides. Consider simplifying this or documenting why this specific case needs to be handled.
| } | ||
|
|
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indentation is inconsistent here - this line should be indented to the same level as the if statement above (line 105). The closing brace on line 119 is properly indented, but the if statement starting on line 112 has extra indentation.
| } |
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Nodes; | ||
| using Microsoft.DurableTask.Converters; | ||
|
|
||
| namespace Microsoft.DurableTask.Tests.Converters; | ||
|
|
||
| public class JsonDataConverterTests | ||
| { | ||
| // Test types matching ConsoleAppMinimal sample | ||
| public record ComponentContext(string Name, string Type, List<string> Dependencies); | ||
| public record PlanResult(bool Success, int Status, string Reason); | ||
| public record TestActivityInput(Dictionary<string, object> Properties); | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_DictionaryWithComplexTypes_PreservesTypes() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var componentContext = new ComponentContext( | ||
| Name: "loganalytics", | ||
| Type: "terraform", | ||
| Dependencies: ["resourcegroup"]); | ||
|
|
||
| var planResult = new PlanResult( | ||
| Success: true, | ||
| Status: 2, | ||
| Reason: "replace_because_tainted"); | ||
|
|
||
| var input = new TestActivityInput(new Dictionary<string, object> | ||
| { | ||
| { "ComponentContext", componentContext }, | ||
| { "PlanResult", planResult } | ||
| }); | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| TestActivityInput? deserialized = converter.Deserialize<TestActivityInput>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| deserialized!.Properties.Should().NotBeNull().And.NotBeEmpty(); | ||
| deserialized.Properties.Should().HaveCount(2); | ||
|
|
||
| // Verify ComponentContext is preserved (not JsonElement) | ||
| deserialized.Properties["ComponentContext"].Should().BeOfType<ComponentContext>(); | ||
| var deserializedComponent = (ComponentContext)deserialized.Properties["ComponentContext"]; | ||
| deserializedComponent.Name.Should().Be("loganalytics"); | ||
| deserializedComponent.Type.Should().Be("terraform"); | ||
| deserializedComponent.Dependencies.Should().Equal(["resourcegroup"]); | ||
|
|
||
| // Verify PlanResult is preserved (not JsonElement) | ||
| deserialized.Properties["PlanResult"].Should().BeOfType<PlanResult>(); | ||
| var deserializedPlan = (PlanResult)deserialized.Properties["PlanResult"]; | ||
| deserializedPlan.Success.Should().BeTrue(); | ||
| deserializedPlan.Status.Should().Be(2); | ||
| deserializedPlan.Reason.Should().Be("replace_because_tainted"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_DirectComplexType_PreservesType() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var componentContext = new ComponentContext( | ||
| Name: "test", | ||
| Type: "type", | ||
| Dependencies: ["dep1", "dep2"]); | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(componentContext); | ||
| ComponentContext? deserialized = converter.Deserialize<ComponentContext>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| ComponentContext result = deserialized!; | ||
| result.Name.Should().Be("test"); | ||
| result.Type.Should().Be("type"); | ||
| result.Dependencies.Should().Equal(["dep1", "dep2"]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_DictionaryWithPrimitives_PreservesTypes() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var input = new Dictionary<string, object> | ||
| { | ||
| { "StringValue", "test" }, | ||
| { "IntValue", 42 }, | ||
| { "BoolValue", true }, | ||
| { "DoubleValue", 3.14 }, | ||
| { "NullValue", null! } | ||
| }; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| Dictionary<string, object>? deserialized = converter.Deserialize<Dictionary<string, object>>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| Dictionary<string, object> result = deserialized!; | ||
| result.Should().HaveCount(5); | ||
| // Note: Primitives in Dictionary<string, object> are deserialized as JsonElement | ||
| // because they don't have type metadata (primitives are not wrapped). | ||
| // This is expected behavior - the converter only wraps complex types. | ||
| result["StringValue"].Should().BeOfType<JsonElement>().Subject.GetString().Should().Be("test"); | ||
| result["IntValue"].Should().BeOfType<JsonElement>().Subject.GetInt32().Should().Be(42); | ||
| result["BoolValue"].Should().BeOfType<JsonElement>().Subject.GetBoolean().Should().Be(true); | ||
| result["DoubleValue"].Should().BeOfType<JsonElement>().Subject.GetDouble().Should().BeApproximately(3.14, 0.01); | ||
| result["NullValue"].Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_DictionaryWithNestedObjects_PreservesTypes() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var inner = new ComponentContext("inner", "type", ["dep"]); | ||
| var input = new Dictionary<string, object> | ||
| { | ||
| { "Outer", new Dictionary<string, object> { { "Inner", inner } } } | ||
| }; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| Dictionary<string, object>? deserialized = converter.Deserialize<Dictionary<string, object>>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| Dictionary<string, object> result = deserialized!; | ||
| var outer = result["Outer"].Should().BeOfType<Dictionary<string, object>>().Subject; | ||
| var innerDeserialized = outer["Inner"].Should().BeOfType<ComponentContext>().Subject; | ||
| innerDeserialized.Name.Should().Be("inner"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_ArrayOfComplexTypes_PreservesTypes() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var items = new ComponentContext[] | ||
| { | ||
| new("item1", "type1", ["dep1"]), | ||
| new("item2", "type2", ["dep2"]) | ||
| }; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(items); | ||
| ComponentContext[]? deserialized = converter.Deserialize<ComponentContext[]>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| ComponentContext[] result = deserialized!; | ||
| result.Should().HaveCount(2); | ||
| result[0].Name.Should().Be("item1"); | ||
| result[1].Name.Should().Be("item2"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_DictionaryWithArray_PreservesTypes() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var input = new Dictionary<string, object> | ||
| { | ||
| { "Items", new ComponentContext[] | ||
| { | ||
| new("item1", "type1", ["dep1"]), | ||
| new("item2", "type2", ["dep2"]) | ||
| } } | ||
| }; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| Dictionary<string, object>? deserialized = converter.Deserialize<Dictionary<string, object>>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| Dictionary<string, object> result = deserialized!; | ||
| var items = result["Items"].Should().BeOfType<ComponentContext[]>().Subject; | ||
| items.Should().HaveCount(2); | ||
| items[0].Name.Should().Be("item1"); | ||
| items[1].Name.Should().Be("item2"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_NullValue_HandlesCorrectly() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
|
|
||
| // Act | ||
| string? serialized = converter.Serialize(null); | ||
| object? deserialized = converter.Deserialize<object>(serialized); | ||
|
|
||
| // Assert | ||
| serialized.Should().BeNull(); | ||
| deserialized.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_JsonElement_UnwrapsTypeMetadata() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| var input = new Dictionary<string, object> | ||
| { | ||
| { "Key", "Value" } | ||
| }; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| JsonElement deserialized = converter.Deserialize<JsonElement>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.ValueKind.Should().Be(JsonValueKind.Object); | ||
| deserialized.TryGetProperty("Key", out JsonElement keyElement).Should().BeTrue(); | ||
| keyElement.GetString().Should().Be("Value"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_ObjectArray_DoesNotWrap() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| object[] input = ["item1", "item2", "item3"]; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| object[]? deserialized = converter.Deserialize<object[]>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| object[] result = deserialized!; | ||
| result.Should().HaveCount(3); | ||
| // Note: Elements in object[] may be deserialized as JsonElement if they don't have type metadata | ||
| // This is expected behavior - object[] arrays are not wrapped to maintain raw JSON format | ||
| result[0].Should().BeOfType<JsonElement>().Subject.GetString().Should().Be("item1"); | ||
| result[1].Should().BeOfType<JsonElement>().Subject.GetString().Should().Be("item2"); | ||
| result[2].Should().BeOfType<JsonElement>().Subject.GetString().Should().Be("item3"); | ||
|
|
||
| // Verify it's not wrapped with type metadata (should be raw JSON array) | ||
| serialized.Should().StartWith("["); | ||
| serialized.Should().EndWith("]"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SerializeDeserialize_JsonNode_DoesNotWrap() | ||
| { | ||
| // Arrange | ||
| var converter = JsonDataConverter.Default; | ||
| JsonNode input = JsonNode.Parse("""{"key": "value"}""")!; | ||
|
|
||
| // Act | ||
| string serialized = converter.Serialize(input); | ||
| JsonNode? deserialized = converter.Deserialize<JsonNode>(serialized); | ||
|
|
||
| // Assert | ||
| deserialized.Should().NotBeNull(); | ||
| deserialized!["key"]!.GetValue<string>().Should().Be("value"); | ||
|
|
||
| // Verify it's not wrapped with type metadata | ||
| serialized.Should().Contain("\"key\""); | ||
| serialized.Should().Contain("\"value\""); | ||
| serialized.Should().NotContain("$type"); | ||
| } | ||
| } | ||
|
|
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no test coverage for the security-sensitive Type.GetType paths that deserialize untrusted type metadata. Consider adding tests that verify behavior when provided with malformed or malicious type names (e.g., non-existent types, types from unexpected assemblies, or types that should not be allowed). This would help ensure the security of the deserialization process.
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections; |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The using directive for System.Collections appears to be unused. The code only uses ICollection, IEnumerable, IList, List, Dictionary<TKey, TValue>, and IDictionary<TKey, TValue>, all of which are in System.Collections.Generic (already imported on line 5). Consider removing this unused using directive.
| using System.Collections; |
| foreach (JsonConverter converter in options.Converters) | ||
| { | ||
| if (converter is ObjectWithTypeMetadataJsonConverterFactory) | ||
| { | ||
| hasConverter = true; | ||
| break; | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| if (wrappedType != null && targetType.IsArray && valueElement.ValueKind == JsonValueKind.Array) | ||
| { | ||
| // Check if wrapped type is a generic collection that implements IEnumerable<T> | ||
| if (wrappedType.IsGenericType) | ||
| { | ||
| Type genericTypeDef = wrappedType.GetGenericTypeDefinition(); | ||
| if (genericTypeDef == typeof(List<>) || | ||
| genericTypeDef == typeof(IList<>) || | ||
| genericTypeDef == typeof(ICollection<>) || | ||
| genericTypeDef == typeof(IEnumerable<>)) | ||
| { | ||
| Type[] genericArgs = wrappedType.GetGenericArguments(); | ||
| if (genericArgs.Length == 1) | ||
| { | ||
| Type elementType = genericArgs[0]; | ||
| Type targetElementType = targetType.GetElementType()!; | ||
|
|
||
| // If the element types match, we can deserialize directly | ||
| if (elementType == targetElementType) | ||
| { | ||
| string unwrappedJson = valueElement.GetRawText(); | ||
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | ||
| } | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (wrappedType != null && targetType.IsArray && valueElement.ValueKind == JsonValueKind.Array) | |
| { | |
| // Check if wrapped type is a generic collection that implements IEnumerable<T> | |
| if (wrappedType.IsGenericType) | |
| { | |
| Type genericTypeDef = wrappedType.GetGenericTypeDefinition(); | |
| if (genericTypeDef == typeof(List<>) || | |
| genericTypeDef == typeof(IList<>) || | |
| genericTypeDef == typeof(ICollection<>) || | |
| genericTypeDef == typeof(IEnumerable<>)) | |
| { | |
| Type[] genericArgs = wrappedType.GetGenericArguments(); | |
| if (genericArgs.Length == 1) | |
| { | |
| Type elementType = genericArgs[0]; | |
| Type targetElementType = targetType.GetElementType()!; | |
| // If the element types match, we can deserialize directly | |
| if (elementType == targetElementType) | |
| { | |
| string unwrappedJson = valueElement.GetRawText(); | |
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | |
| } | |
| } | |
| } | |
| } | |
| if ( | |
| wrappedType != null && | |
| targetType.IsArray && | |
| valueElement.ValueKind == JsonValueKind.Array && | |
| wrappedType.IsGenericType && | |
| ( | |
| wrappedType.GetGenericTypeDefinition() == typeof(List<>) || | |
| wrappedType.GetGenericTypeDefinition() == typeof(IList<>) || | |
| wrappedType.GetGenericTypeDefinition() == typeof(ICollection<>) || | |
| wrappedType.GetGenericTypeDefinition() == typeof(IEnumerable<>) | |
| ) && | |
| wrappedType.GetGenericArguments().Length == 1 && | |
| wrappedType.GetGenericArguments()[0] == targetType.GetElementType() | |
| ) | |
| { | |
| string unwrappedJson = valueElement.GetRawText(); | |
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); |
| if (wrappedType != null && wrappedType.IsArray && !targetType.IsArray) | ||
| { | ||
| if (valueElement.ValueKind == JsonValueKind.Array && valueElement.GetArrayLength() > 0) | ||
| { | ||
| // Get the first element of the array | ||
| JsonElement firstElement = valueElement[0]; | ||
| string firstElementJson = firstElement.GetRawText(); | ||
|
|
||
| // Check if the first element is also wrapped | ||
| using (JsonDocument innerDoc = JsonDocument.Parse(firstElementJson)) | ||
| { | ||
| JsonElement innerRoot = innerDoc.RootElement; | ||
| if (innerRoot.ValueKind == JsonValueKind.Object && | ||
| innerRoot.TryGetProperty("$type", out JsonElement innerTypeElement) && | ||
| innerRoot.TryGetProperty("$value", out JsonElement innerValueElement)) | ||
| { | ||
| string innerTypeName = innerTypeElement.GetString()!; | ||
| Type? innerWrappedType = Type.GetType(innerTypeName, throwOnError: false); | ||
|
|
||
| if (innerWrappedType != null && innerWrappedType == targetType) | ||
| { | ||
| // Unwrap the inner object | ||
| string unwrappedJson = innerValueElement.GetRawText(); | ||
| return JsonSerializer.Deserialize(unwrappedJson, targetType, this.options); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // First element is not wrapped, try to deserialize it directly | ||
| // This handles cases where the array contains a single unwrapped element | ||
| try | ||
| { | ||
| return JsonSerializer.Deserialize(firstElementJson, targetType, this.options); | ||
| } | ||
| catch | ||
| { | ||
| // If deserialization fails, fall through to normal deserialization | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
No description provided.