diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index 73e479c..9985071 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -8,9 +8,9 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap messageTraitFixedFields = new() { - { "headers", (a, n) => { a.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(n) }; } }, + { "headers", (a, n) => { (a.Headers ??= new AsyncApiMultiFormatSchema()).Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, - { "schemaFormat", (a, n) => { a.Headers.SchemaFormat = n.GetScalarValue(); } }, + { "schemaFormat", (a, n) => { (a.Headers ??= new AsyncApiMultiFormatSchema()).SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs index 82d3867..bf2f991 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs @@ -76,9 +76,9 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteStartObject(); //writer.WriteOptionalProperty(AsyncApiConstants.MessageId, this.MessageId); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers?.Schema, (w, h) => h.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Headers.SchemaFormat); + writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Headers?.SchemaFormat); writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); @@ -100,7 +100,7 @@ public virtual void SerializeV3(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers?.Schema == null ? null : this.Headers, (w, h) => h.SerializeV3(w)); writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w)); writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index e77acf5..39d38c7 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -87,6 +87,109 @@ public void V2_AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError diagnostic.Errors.First().Message.Should().StartWith("Could not deserialize Payload. Supported formats are"); } + [Test] + public void V2_AsyncApiMessageTrait_WithoutHeaders_Serializes() + { + var expected = + """ + name: TraitName + """; + + var trait = new AsyncApiMessageTrait { Name = "TraitName" }; + + var actual = trait.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + actual.Should().BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void V2_AsyncApiMessageTrait_WithSchemaFormatBeforeHeaders_Deserializes() + { + var input = + """ + schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0 + headers: + type: object + """; + + var trait = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + trait.Headers.SchemaFormat.Should().Be("application/vnd.aai.asyncapi+json;version=2.6.0"); + trait.Headers.Schema.Should().BeOfType(); + } + + [Test] + public void V2_AsyncApiMessageTrait_WithSchemaFormatAndNoHeaders_Deserializes() + { + var input = + """ + schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0 + """; + + var trait = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + trait.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().BePlatformAgnosticEquivalentTo(input); + } + + [Test] + public void V3_AsyncApiMessageTrait_WithoutHeaders_Serializes() + { + var expected = + """ + name: TraitName + """; + + var trait = new AsyncApiMessageTrait { Name = "TraitName" }; + + var actual = trait.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); + + actual.Should().BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void V3_AsyncApiMessageTrait_WithHeaders_Reserializes() + { + var input = + """ + headers: + type: object + name: TraitName + """; + + // By design, 3.x schemas are always read as multi-format schemas with the default schema format filled in, so the headers come back in the explicit multi-format form. + var expected = + """ + headers: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: object + name: TraitName + """; + + var trait = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi3_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + trait.Headers.Schema.Should().BeOfType(); + trait.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void V2_AsyncApiMessageTrait_WithSchemaFormatAndNoHeaders_UpgradesToV3WithoutHeaders() + { + var input = + """ + schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0 + name: TraitName + """; + + var trait = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + trait.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().BePlatformAgnosticEquivalentTo("name: TraitName"); + } + [Test] public void V2_AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() {