Skip to content

Commit 9251ff6

Browse files
bieluclaude
andcommitted
fix: handle 2.x message traits without headers
The 2.x writer threw NullReferenceException for a message trait without headers because it read Headers.SchemaFormat unconditionally. The 2.x reader threw too when schemaFormat appeared without headers, or before them, and a later headers field discarded an earlier schemaFormat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ee29e69 commit 9251ff6

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ internal static partial class AsyncApiV2Deserializer
88
{
99
private static FixedFieldMap<AsyncApiMessageTrait> messageTraitFixedFields = new()
1010
{
11-
{ "headers", (a, n) => { a.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(n) }; } },
11+
{ "headers", (a, n) => { (a.Headers ??= new AsyncApiMultiFormatSchema()).Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } },
1212
{ "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } },
13-
{ "schemaFormat", (a, n) => { a.Headers.SchemaFormat = n.GetScalarValue(); } },
13+
{ "schemaFormat", (a, n) => { (a.Headers ??= new AsyncApiMultiFormatSchema()).SchemaFormat = n.GetScalarValue(); } },
1414
{ "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } },
1515
{ "name", (a, n) => { a.Name = n.GetScalarValue(); } },
1616
{ "title", (a, n) => { a.Title = n.GetScalarValue(); } },

‎src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ public virtual void SerializeV2(IAsyncApiWriter writer)
7676

7777
writer.WriteStartObject();
7878
//writer.WriteOptionalProperty(AsyncApiConstants.MessageId, this.MessageId);
79-
writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w));
79+
writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers?.Schema, (w, h) => h.SerializeV2(w));
8080
writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w));
81-
writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Headers.SchemaFormat);
81+
writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Headers?.SchemaFormat);
8282
writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType);
8383
writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name);
8484
writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title);
@@ -100,7 +100,7 @@ public virtual void SerializeV3(IAsyncApiWriter writer)
100100
}
101101

102102
writer.WriteStartObject();
103-
writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w));
103+
writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers?.Schema == null ? null : this.Headers, (w, h) => h.SerializeV3(w));
104104
writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w));
105105
writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType);
106106
writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name);

‎test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,52 @@ public void V2_AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError
8787
diagnostic.Errors.First().Message.Should().StartWith("Could not deserialize Payload. Supported formats are");
8888
}
8989

90+
[Test]
91+
public void V2_AsyncApiMessageTrait_WithoutHeaders_Serializes()
92+
{
93+
var expected =
94+
"""
95+
name: TraitName
96+
""";
97+
98+
var trait = new AsyncApiMessageTrait { Name = "TraitName" };
99+
100+
var actual = trait.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);
101+
102+
actual.Should().BePlatformAgnosticEquivalentTo(expected);
103+
}
104+
105+
[Test]
106+
public void V2_AsyncApiMessageTrait_WithSchemaFormatBeforeHeaders_Deserializes()
107+
{
108+
var input =
109+
"""
110+
schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0
111+
headers:
112+
type: object
113+
""";
114+
115+
var trait = new AsyncApiStringReader().ReadFragment<AsyncApiMessageTrait>(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic);
116+
117+
diagnostic.Errors.Should().BeEmpty();
118+
trait.Headers.SchemaFormat.Should().Be("application/vnd.aai.asyncapi+json;version=2.6.0");
119+
trait.Headers.Schema.Should().BeOfType<AsyncApiJsonSchema>();
120+
}
121+
122+
[Test]
123+
public void V2_AsyncApiMessageTrait_WithSchemaFormatAndNoHeaders_Deserializes()
124+
{
125+
var input =
126+
"""
127+
schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0
128+
""";
129+
130+
var trait = new AsyncApiStringReader().ReadFragment<AsyncApiMessageTrait>(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic);
131+
132+
diagnostic.Errors.Should().BeEmpty();
133+
trait.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should().BePlatformAgnosticEquivalentTo(input);
134+
}
135+
90136
[Test]
91137
public void V2_AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat()
92138
{

0 commit comments

Comments
 (0)