Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,63 @@ public void AddSystemTextJsonSerializationTest()
serializer = _services.GetService<ISerializer<SimpleClass>>();
serializer.Should().NotBeNull();
}

[TestMethod]
public void StringSerializationWithRegisteredTypeInfoTest()
{
// This test validates the fix for the JsonSerializerIsReflectionDisabled issue
// where serializing strings would fail in AOT scenarios without registered type info
var context = new HostBuilderContext(new Dictionary<object, object>());
var services = new ServiceCollection();
services.AddSystemTextJsonSerialization(context);
var serviceProvider = services.BuildServiceProvider();

var serializer = serviceProvider.GetRequiredService<ISerializer>();
serializer.Should().NotBeNull();

// Test string serialization
const string testValue = "test token value";
var serialized = serializer.ToString(testValue, typeof(string));
serialized.Should().NotBeNullOrEmpty();

var deserialized = serializer.FromString(serialized, typeof(string));
deserialized.Should().Be(testValue);

// Test string[] serialization
var testArray = new[] { "value1", "value2", "value3" };
var serializedArray = serializer.ToString(testArray, typeof(string[]));
serializedArray.Should().NotBeNullOrEmpty();

var deserializedArray = serializer.FromString(serializedArray, typeof(string[]));
deserializedArray.Should().BeEquivalentTo(testArray);

// Test bool serialization
var serializedBool = serializer.ToString(true, typeof(bool));
serializedBool.Should().NotBeNullOrEmpty();

var deserializedBool = serializer.FromString(serializedBool, typeof(bool));
deserializedBool.Should().Be(true);
}

[TestMethod]
public void StringSerializerExtensionsWithRegisteredTypeInfoTest()
{
// This test validates that the SerializerExtensions.ToString<T> method works
// for string types with the registered type info (same path as KeyValueStorage)
var context = new HostBuilderContext(new Dictionary<object, object>());
var services = new ServiceCollection();
services.AddSystemTextJsonSerialization(context);
var serviceProvider = services.BuildServiceProvider();

var serializer = serviceProvider.GetRequiredService<ISerializer>();
serializer.Should().NotBeNull();

// Test using extension method (same as ApplicationDataKeyValueStorage.Serialize<string>)
const string testValue = "authentication_token_12345";
var serialized = serializer.ToString(testValue);
serialized.Should().NotBeNullOrEmpty();

var deserialized = serializer.FromString<string>(serialized);
deserialized.Should().Be(testValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<!-- Disable JSON reflection to test AOT-compatible serialization -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace Uno.Extensions.Serialization;

/// <summary>
/// A source-generated JSON serializer context for common types used internally by the serialization infrastructure.
/// This enables AOT-compatible serialization for types like string, string arrays, and bool
/// without requiring reflection-based JSON serialization.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(bool))]
internal sealed partial class CommonTypesJsonSerializerContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ public static IServiceCollection AddSystemTextJsonSerialization(
})
.AddSingleton<SystemTextJsonSerializer>()
.AddSingleton<ISerializer>(services => services.GetRequiredService<SystemTextJsonSerializer>())
.AddSingleton(typeof(ISerializer<>), typeof(SystemTextJsonGeneratedSerializer<>));
.AddSingleton(typeof(ISerializer<>), typeof(SystemTextJsonGeneratedSerializer<>))
// Register JSON type info for common types to support AOT scenarios where reflection is disabled
.AddJsonTypeInfo(CommonTypesJsonSerializerContext.Default.String)
.AddJsonTypeInfo(CommonTypesJsonSerializerContext.Default.StringArray)
.AddJsonTypeInfo(CommonTypesJsonSerializerContext.Default.Boolean);
}

/// <summary>
Expand Down
35 changes: 33 additions & 2 deletions src/Uno.Extensions.Serialization/SystemTextJsonSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;

namespace Uno.Extensions.Serialization;

Expand All @@ -7,7 +8,7 @@ namespace Uno.Extensions.Serialization;
/// </summary>
public class SystemTextJsonSerializer : ISerializer
{
private readonly JsonSerializerOptions? _serializerOptions;
private readonly JsonSerializerOptions _serializerOptions;
private readonly IServiceProvider _services;

private ISerializerTypedInstance? TypedSerializer(Type jsonType) => _services.GetServices<ISerializerTypedInstance>().FirstOrDefault(x => x.JsonType == jsonType);
Expand All @@ -24,7 +25,37 @@ public class SystemTextJsonSerializer : ISerializer
public SystemTextJsonSerializer(IServiceProvider services, JsonSerializerOptions? serializerOptions = null)
{
_services = services;
_serializerOptions = serializerOptions;
_serializerOptions = ConfigureSerializerOptions(serializerOptions);
}

private static JsonSerializerOptions ConfigureSerializerOptions(JsonSerializerOptions? options)
{
// Create a new options instance or clone the provided one to avoid modifying shared instances
var configuredOptions = options is null
? new JsonSerializerOptions()
: new JsonSerializerOptions(options);

// Configure TypeInfoResolver to support both reflection-based and AOT scenarios.
// Use DefaultJsonTypeInfoResolver for reflection-based serialization combined with
// CommonTypesJsonSerializerContext for common types that have source-generated metadata.
if (configuredOptions.TypeInfoResolver is null)
{
configuredOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(
new DefaultJsonTypeInfoResolver(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultJsonTypeInfoResolver itself is problematic in a trimming context! From the docs: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.defaultjsontypeinforesolver?view=net-10.0

Defines the default, reflection-based JSON contract resolver used by System.Text.Json.

(Emphasis added.)

You cannot reliably use DefaultJsonTypeInfoResolver in a trimmable environment.

Use of DefaultJsonTypeInfoResolver in and of itself invalidates this as a fix!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also note the docs for the constructor: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.defaultjsontypeinforesolver.-ctor?view=net-10.0

which notes:

[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
public DefaultJsonTypeInfoResolver();

which is the trimmer way of saying "this requires Reflection, y'all!"

CommonTypesJsonSerializerContext.Default);
}
else
{
// User provided a resolver - place it first so it has priority.
// DefaultJsonTypeInfoResolver and CommonTypesJsonSerializerContext serve as fallbacks
// for types not handled by the user's resolver.
configuredOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(
configuredOptions.TypeInfoResolver,
new DefaultJsonTypeInfoResolver(),
CommonTypesJsonSerializerContext.Default);
}

return configuredOptions;
}

/// <summary>
Expand Down
Loading