diff --git a/app/MindWork AI Studio/Tools/DataBackup/DataBackupFormat.cs b/app/MindWork AI Studio/Tools/DataBackup/DataBackupFormat.cs
index 60db85142..f487a68b4 100644
--- a/app/MindWork AI Studio/Tools/DataBackup/DataBackupFormat.cs
+++ b/app/MindWork AI Studio/Tools/DataBackup/DataBackupFormat.cs
@@ -51,12 +51,13 @@ public static class DataBackupFormat
/// How the manifest and the plan of a restore are written: readable for anybody who opens them,
/// and for the runtime, which applies the plan.
///
- public static readonly JsonSerializerOptions JSON_OPTIONS = new()
+ public static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters = { new JsonStringEnumConverter() },
- };
+ TypeInfoResolver = DataBackupJsonContext.Default,
+ }.AsReadOnly();
///
/// File types which are compressed already. Compressing them once more costs time and makes the
diff --git a/app/MindWork AI Studio/Tools/DataBackup/DataBackupJsonContext.cs b/app/MindWork AI Studio/Tools/DataBackup/DataBackupJsonContext.cs
new file mode 100644
index 000000000..1289f9818
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/DataBackup/DataBackupJsonContext.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools.DataBackup;
+
+///
+/// The contracts with which System.Text.Json reads and writes the manifest of a backup and the plan and report of
+/// a restore, built at compile time.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+[JsonSerializable(typeof(DataBackupManifest))]
+[JsonSerializable(typeof(DataRestorePlan))]
+[JsonSerializable(typeof(DataRestoreReport))]
+internal sealed partial class DataBackupJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/ProvenanceJsonContext.cs b/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/ProvenanceJsonContext.cs
new file mode 100644
index 000000000..4831a6169
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/ProvenanceJsonContext.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools.Databases.ProvenanceStore;
+
+///
+/// The contracts with which System.Text.Json reads and writes the content around a passage, built at compile time.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(List))]
+internal sealed partial class ProvenanceJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/SqliteProvenanceStoreClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/SqliteProvenanceStoreClientImplementation.cs
index b7e198555..2414d0f25 100644
--- a/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/SqliteProvenanceStoreClientImplementation.cs
+++ b/app/MindWork AI Studio/Tools/Databases/ProvenanceStore/SqliteProvenanceStoreClientImplementation.cs
@@ -39,7 +39,11 @@ public sealed class SqliteProvenanceStoreClientImplementation(string name, strin
/// Writes the content around a passage the way it reads: the default encoder would turn every
/// character outside ASCII into an escape, cf. ToolExecutionResult.MODEL_CONTENT_OPTIONS.
///
- internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
+ internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ TypeInfoResolver = ProvenanceJsonContext.Default,
+ }.AsReadOnly();
private readonly string databasePath = databasePath;
private readonly string lastCleanupPath = Path.Join(basePath, LAST_CLEANUP_FILENAME);
@@ -351,7 +355,7 @@ public sealed class SqliteProvenanceStoreClientImplementation(string name, strin
try
{
- return JsonSerializer.Deserialize>(row.SurroundingContent) ?? [];
+ return JsonSerializer.Deserialize>(row.SurroundingContent, JSON_OPTIONS) ?? [];
}
catch (JsonException exception)
{
diff --git a/app/Tests/Serialization/JsonCoverageTests.cs b/app/Tests/Serialization/JsonCoverageTests.cs
new file mode 100644
index 000000000..b900e49f3
--- /dev/null
+++ b/app/Tests/Serialization/JsonCoverageTests.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tests.Serialization;
+
+///
+/// Checks that every contract AI Studio uses for its JSON comes from source generation, never from reflection.
+///
+///
+/// A type which no context of the options knows makes System.Text.Json throw, unless the options fall back to
+/// reflection. Such a fallback hides the missing type until a trimmed build removes what reflection needs, and
+/// only a published build shows it then.
+///
+[TestFixture]
+public sealed class JsonCoverageTests
+{
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
+ public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
+ {
+ var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
+ .Where(type => root.Options.GetTypeInfo(type).OriginatingResolver is not JsonSerializerContext)
+ .Select(JsonContractDescription.TypeName);
+
+ Assert.That(typesWithoutGeneratedContract, Is.Empty);
+ }
+}
\ No newline at end of file