From fa8bb2781f4eba926344ae598244a18a50918b90 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel - bielu <2244074+bielu@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:24:30 +0100 Subject: [PATCH] fix: stop adding topic and retain when writing an MQTT lastWill LastWill wrote `topic` with WriteRequiredProperty and `retain` from a non-nullable bool, so reading `lastWill: { qos: 1 }` and writing it back produced `topic:` (null) and `retain: false`. Both fields are optional in the MQTT binding spec. `retain` now has a nullable backing field (same pattern as MQTTOperationBinding), and both fields are written only when set. The public `Retain` property keeps its bool type, so this is not a breaking API change. Co-Authored-By: Claude Opus 5.5 --- .../MQTT/LastWill.cs | 12 ++++++-- .../MQTT/MQTTBindingVariants_Should.cs | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/LastWill.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/LastWill.cs index 4b77f93..06e5fb6 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/LastWill.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/LastWill.cs @@ -6,6 +6,8 @@ public class LastWill : IAsyncApiElement { + private bool? retain; + /// /// The topic where the Last Will and Testament message will be sent. /// @@ -26,7 +28,11 @@ public class LastWill : IAsyncApiElement /// /// Whether the broker should retain the Last Will and Testament message or not. /// - public bool Retain { get; set; } + public bool Retain + { + get => this.retain ?? false; + set => this.retain = value; + } public void Serialize(IAsyncApiWriter writer) { @@ -36,10 +42,10 @@ public void Serialize(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteRequiredProperty("topic", this.Topic); + writer.WriteOptionalProperty("topic", this.Topic); writer.WriteOptionalProperty("qos", (int?)this.QoS); writer.WriteOptionalProperty("message", this.Message); - writer.WriteRequiredProperty("retain", this.Retain); + writer.WriteOptionalProperty("retain", this.retain); writer.WriteEndObject(); } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs index 8c3f241..68d5063 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/MQTT/MQTTBindingVariants_Should.cs @@ -111,5 +111,35 @@ public void OperationBinding_OmitsUnsetQosAndRetain() var yaml = new MQTTOperationBinding().SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); yaml.Should().NotContain("qos:").And.NotContain("retain:"); } + + [TestCase(AsyncApiVersion.AsyncApi2_0)] + [TestCase(AsyncApiVersion.AsyncApi3_0)] + public void LastWill_WithoutTopicAndRetain_RoundTripsWithoutAddingThem(AsyncApiVersion version) + { + var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.MQTT }; + var yaml = (version == AsyncApiVersion.AsyncApi2_0 ? "url" : "host") + ": localhost\n" + + """ + protocol: mqtt + bindings: + mqtt: + lastWill: + qos: 1 + """; + + var server = new AsyncApiStringReader(settings).ReadFragment(yaml, version, out var diagnostics); + diagnostics.Errors.Should().BeEmpty(); + + var actual = server.SerializeAsYaml(version); + actual.Should().Contain("qos: 1").And.NotContain("topic:").And.NotContain("retain:"); + } + + [Test] + public void LastWill_WithExplicitRetainFalse_WritesRetain() + { + var lastWill = new LastWill { Topic = "/last-wills", Retain = false }; + var server = new MQTTServerBinding { LastWill = lastWill }; + + server.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("topic: /last-wills").And.Contain("retain: false"); + } } }