From 737e37598f7ea1ad562571764950e34a737c0ffb Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 17:34:15 +0200 Subject: [PATCH 1/3] fix: correct ReleaseNotes deserialization for AutoUpdater (fixes #1039) - TypeScript normalize() now maps string releaseNotes to { note } objects and also handles arrays of strings (both cases were broken in PR #1041) - Added ReleaseNotesConverter (JsonConverter) as defensive C# layer that handles all shapes: null, string, string[], object[] - Added [JsonConverter] attribute on UpdateInfo.ReleaseNotes - Added unit tests (no Electron required) covering all four input shapes --- .../API/Entities/UpdateInfo.cs | 6 +- .../Converter/ReleaseNotesConverter.cs | 89 ++++++++++++++++++ src/ElectronNET.Host/api/autoUpdater.ts | 6 +- .../Tests/UpdateInfoSerializationTests.cs | 90 +++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/ElectronNET.API/Converter/ReleaseNotesConverter.cs create mode 100644 src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs diff --git a/src/ElectronNET.API/API/Entities/UpdateInfo.cs b/src/ElectronNET.API/API/Entities/UpdateInfo.cs index c3b6a457..7335e8a1 100644 --- a/src/ElectronNET.API/API/Entities/UpdateInfo.cs +++ b/src/ElectronNET.API/API/Entities/UpdateInfo.cs @@ -1,4 +1,7 @@ -namespace ElectronNET.API.Entities +using System.Text.Json.Serialization; +using ElectronNET.Converter; + +namespace ElectronNET.API.Entities { /// /// @@ -24,6 +27,7 @@ public class UpdateInfo /// /// Gets or sets the release notes. /// + [JsonConverter(typeof(ReleaseNotesConverter))] public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0]; /// diff --git a/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs b/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs new file mode 100644 index 00000000..bdd1c807 --- /dev/null +++ b/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs @@ -0,0 +1,89 @@ +using ElectronNET.API.Entities; +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ElectronNET.Converter; + +/// +/// Handles the polymorphic shape of releaseNotes coming from electron-builder. +/// Depending on the updater.fullChangelog setting, electron-builder sends: +/// - null → when there are no notes +/// - "some string" → plain string (FullChangelog = false, default) +/// - ["note A", "note B"] → array of strings (after broken normalize in older TS) +/// - [{ version, note }, ...] → array of objects (FullChangelog = true) +/// All forms are normalised to ReleaseNoteInfo[] so the C# model stays clean. +/// See: https://github.com/ElectronNET/Electron.NET/issues/1039 +/// +public class ReleaseNotesConverter : JsonConverter +{ + // Ensure the converter is called even when the JSON token is null, + // so we can return an empty array instead of null. + public override bool HandleNull => true; + + public override ReleaseNoteInfo[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return Array.Empty(); + + case JsonTokenType.String: + // Plain string: "Some release notes" + return new[] { new ReleaseNoteInfo { Note = reader.GetString() } }; + + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.String) + { + // Array of strings: ["Note A", "Note B"] + list.Add(new ReleaseNoteInfo { Note = reader.GetString() }); + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + // Array of objects: [{ "version": "1.0", "note": "..." }] + using var doc = JsonDocument.ParseValue(ref reader); + var entry = new ReleaseNoteInfo(); + if (doc.RootElement.TryGetProperty("version", out var version)) + entry.Version = version.GetString(); + if (doc.RootElement.TryGetProperty("note", out var note)) + entry.Note = note.GetString(); + list.Add(entry); + } + else + { + reader.Skip(); + } + } + return list.ToArray(); + + default: + throw new JsonException($"Unexpected token {reader.TokenType} when reading releaseNotes."); + } + } + + public override void Write(Utf8JsonWriter writer, ReleaseNoteInfo[] value, JsonSerializerOptions options) + { + if (value is null || value.Length == 0) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + foreach (var item in value) + { + writer.WriteStartObject(); + if (item.Version is not null) + { + writer.WriteString("version", item.Version); + } + writer.WriteString("note", item.Note); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } +} diff --git a/src/ElectronNET.Host/api/autoUpdater.ts b/src/ElectronNET.Host/api/autoUpdater.ts index 7b9fab6e..d4b34f06 100644 --- a/src/ElectronNET.Host/api/autoUpdater.ts +++ b/src/ElectronNET.Host/api/autoUpdater.ts @@ -5,7 +5,11 @@ let electronSocket: Socket; function normalize(updateInfo) { if (typeof updateInfo?.releaseNotes === "string") { - updateInfo.releaseNotes = [updateInfo.releaseNotes]; + updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }]; + } else if (Array.isArray(updateInfo?.releaseNotes)) { + updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) => + typeof entry === "string" ? { note: entry } : entry, + ); } } diff --git a/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs b/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs new file mode 100644 index 00000000..80bc9b93 --- /dev/null +++ b/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs @@ -0,0 +1,90 @@ +namespace ElectronNET.IntegrationTests.Tests +{ + using System.Text.Json; + using System.Text.Json.Serialization; + using ElectronNET.API.Entities; + + /// + /// Unit tests for UpdateInfo JSON deserialization. + /// Tests the fix for issue #1039: releaseNotes arrives as string or string[] from electron-builder + /// when FullChangelog is false (default), but the C# model expects ReleaseNoteInfo[]. + /// No Electron runtime is required for these tests. + /// + public class UpdateInfoSerializationTests + { + // camelCase + ignore null — mirrors ElectronJson.Options used in production + private static readonly JsonSerializerOptions Options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + // electron-builder sends a plain string when FullChangelog = false (default) + [Fact] + public void Deserialize_WithStringReleaseNotes_ShouldConvertToSingleEntry() + { + var json = """{"version":"1.2.3","releaseNotes":"Some release notes"}"""; + + var result = JsonSerializer.Deserialize(json, Options); + + result.Should().NotBeNull(); + result.ReleaseNotes.Should().HaveCount(1); + result.ReleaseNotes[0].Note.Should().Be("Some release notes"); + } + + // After the (incorrect) TypeScript normalize: string → ["string"] which is an array of strings, + // not an array of ReleaseNoteInfo objects. The C# model must handle this too. + [Fact] + public void Deserialize_WithArrayOfStringReleaseNotes_ShouldConvertToEntries() + { + var json = """{"version":"1.2.3","releaseNotes":["Note A","Note B"]}"""; + + var result = JsonSerializer.Deserialize(json, Options); + + result.Should().NotBeNull(); + result.ReleaseNotes.Should().HaveCount(2); + result.ReleaseNotes[0].Note.Should().Be("Note A"); + result.ReleaseNotes[1].Note.Should().Be("Note B"); + } + + // When FullChangelog = true, electron-builder sends proper objects; this must keep working. + [Fact] + public void Deserialize_WithProperReleaseNoteObjects_ShouldDeserializeNormally() + { + var json = """{"version":"1.2.3","releaseNotes":[{"version":"1.2.3","note":"Proper note"}]}"""; + + var result = JsonSerializer.Deserialize(json, Options); + + result.Should().NotBeNull(); + result.ReleaseNotes.Should().HaveCount(1); + result.ReleaseNotes[0].Version.Should().Be("1.2.3"); + result.ReleaseNotes[0].Note.Should().Be("Proper note"); + } + + // Null releaseNotes should result in an empty array (matching the default value). + [Fact] + public void Deserialize_WithNullReleaseNotes_ShouldReturnEmptyArray() + { + var json = """{"version":"1.2.3","releaseNotes":null}"""; + + var result = JsonSerializer.Deserialize(json, Options); + + result.Should().NotBeNull(); + result.ReleaseNotes.Should().NotBeNull(); + result.ReleaseNotes.Should().BeEmpty(); + } + + // Absent releaseNotes field should keep the default empty array. + [Fact] + public void Deserialize_WithMissingReleaseNotes_ShouldReturnEmptyArray() + { + var json = """{"version":"1.2.3"}"""; + + var result = JsonSerializer.Deserialize(json, Options); + + result.Should().NotBeNull(); + result.ReleaseNotes.Should().NotBeNull(); + result.ReleaseNotes.Should().BeEmpty(); + } + } +} From d2048992130be43d8332fa80007799f2b5489bab Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 17:54:24 +0200 Subject: [PATCH 2/3] chore: sync generated autoUpdater.js and .map with updated TypeScript source --- src/ElectronNET.Host/api/autoUpdater.js | 5 ++++- src/ElectronNET.Host/api/autoUpdater.js.map | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ElectronNET.Host/api/autoUpdater.js b/src/ElectronNET.Host/api/autoUpdater.js index 457f0890..84cc5683 100644 --- a/src/ElectronNET.Host/api/autoUpdater.js +++ b/src/ElectronNET.Host/api/autoUpdater.js @@ -3,7 +3,10 @@ const electron_updater_1 = require("electron-updater"); let electronSocket; function normalize(updateInfo) { if (typeof updateInfo?.releaseNotes === "string") { - updateInfo.releaseNotes = [updateInfo.releaseNotes]; + updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }]; + } + else if (Array.isArray(updateInfo?.releaseNotes)) { + updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) => typeof entry === "string" ? { note: entry } : entry); } } module.exports = (socket) => { diff --git a/src/ElectronNET.Host/api/autoUpdater.js.map b/src/ElectronNET.Host/api/autoUpdater.js.map index 3387d049..2e3dc29a 100644 --- a/src/ElectronNET.Host/api/autoUpdater.js.map +++ b/src/ElectronNET.Host/api/autoUpdater.js.map @@ -1 +1 @@ -{"version":3,"file":"autoUpdater.js","sourceRoot":"","sources":["autoUpdater.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"autoUpdater.js","sourceRoot":"","sources":["autoUpdater.ts"],"names":[],"mappings":"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"} \ No newline at end of file From a0430c972f0fa0f07f47882b51366f39a762ed6e Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 18:07:10 +0200 Subject: [PATCH 3/3] refactor: address Copilot PR review comments - Fix namespace: ElectronNET.Converter -> ElectronNET.API.Converter - Replace JsonDocument.ParseValue() with JsonSerializer.Deserialize() for cleaner, allocation-free object array parsing - Fix Write(): empty ReleaseNoteInfo[] now serializes as [] instead of null - Use Array.Empty() in UpdateInfo default initializer - Add tests: Serialize_WithEmptyReleaseNotes and Serialize_WithNullReleaseNotes --- .../API/Entities/UpdateInfo.cs | 7 +++--- .../Converter/ReleaseNotesConverter.cs | 18 +++++++------ .../Tests/UpdateInfoSerializationTests.cs | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/ElectronNET.API/API/Entities/UpdateInfo.cs b/src/ElectronNET.API/API/Entities/UpdateInfo.cs index 7335e8a1..d076b4d3 100644 --- a/src/ElectronNET.API/API/Entities/UpdateInfo.cs +++ b/src/ElectronNET.API/API/Entities/UpdateInfo.cs @@ -1,5 +1,6 @@ -using System.Text.Json.Serialization; -using ElectronNET.Converter; +using System; +using System.Text.Json.Serialization; +using ElectronNET.API.Converter; namespace ElectronNET.API.Entities { @@ -28,7 +29,7 @@ public class UpdateInfo /// Gets or sets the release notes. /// [JsonConverter(typeof(ReleaseNotesConverter))] - public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0]; + public ReleaseNoteInfo[] ReleaseNotes { get; set; } = Array.Empty(); /// /// Gets or sets the release date. diff --git a/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs b/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs index bdd1c807..aaaab5d2 100644 --- a/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs +++ b/src/ElectronNET.API/Converter/ReleaseNotesConverter.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace ElectronNET.Converter; +namespace ElectronNET.API.Converter; /// /// Handles the polymorphic shape of releaseNotes coming from electron-builder. @@ -45,12 +45,7 @@ public override ReleaseNoteInfo[] Read(ref Utf8JsonReader reader, Type typeToCon else if (reader.TokenType == JsonTokenType.StartObject) { // Array of objects: [{ "version": "1.0", "note": "..." }] - using var doc = JsonDocument.ParseValue(ref reader); - var entry = new ReleaseNoteInfo(); - if (doc.RootElement.TryGetProperty("version", out var version)) - entry.Version = version.GetString(); - if (doc.RootElement.TryGetProperty("note", out var note)) - entry.Note = note.GetString(); + var entry = JsonSerializer.Deserialize(ref reader, options) ?? new ReleaseNoteInfo(); list.Add(entry); } else @@ -67,12 +62,19 @@ public override ReleaseNoteInfo[] Read(ref Utf8JsonReader reader, Type typeToCon public override void Write(Utf8JsonWriter writer, ReleaseNoteInfo[] value, JsonSerializerOptions options) { - if (value is null || value.Length == 0) + if (value is null) { writer.WriteNullValue(); return; } + if (value.Length == 0) + { + writer.WriteStartArray(); + writer.WriteEndArray(); + return; + } + writer.WriteStartArray(); foreach (var item in value) { diff --git a/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs b/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs index 80bc9b93..613e3666 100644 --- a/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/UpdateInfoSerializationTests.cs @@ -1,5 +1,6 @@ namespace ElectronNET.IntegrationTests.Tests { + using System; using System.Text.Json; using System.Text.Json.Serialization; using ElectronNET.API.Entities; @@ -86,5 +87,29 @@ public void Deserialize_WithMissingReleaseNotes_ShouldReturnEmptyArray() result.ReleaseNotes.Should().NotBeNull(); result.ReleaseNotes.Should().BeEmpty(); } + + // Empty array must serialize as [] not null, so round-trips and downstream + // consumers don't receive unexpected null for a non-null array value. + [Fact] + public void Serialize_WithEmptyReleaseNotes_ShouldProduceEmptyArray() + { + var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = Array.Empty() }; + + var json = JsonSerializer.Serialize(updateInfo, Options); + + json.Should().Contain("\"releaseNotes\":[]"); + } + + // Null value: with DefaultIgnoreCondition.WhenWritingNull the property is + // omitted entirely at the serializer level (before Write() is called). + [Fact] + public void Serialize_WithNullReleaseNotes_ShouldOmitProperty() + { + var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = null }; + + var json = JsonSerializer.Serialize(updateInfo, Options); + + json.Should().NotContain("releaseNotes"); + } } }