Skip to content

Commit f37105c

Browse files
committed
[Host.Serialization.SystemTextJson] Pass the actual type by default on serialization
Signed-off-by: Tomasz Maruszak <[email protected]>
1 parent c87bf39 commit f37105c

File tree

5 files changed

+176
-8
lines changed

5 files changed

+176
-8
lines changed

docs/serialization.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Please read the [Introduction](intro.md) before reading this provider documentat
44

55
- [Configuration](#configuration)
66
- [Json (System.Text.Json)](#json-systemtextjson)
7+
- [Polymorphic Type Handling](#polymorphic-type-handling)
8+
- [Object to Inferred Types Converter](#object-to-inferred-types-converter)
79
- [Json (Newtonsoft.Json)](#json-newtonsoftjson)
810
- [Avro](#avro)
911
- [GoogleProtobuf](#googleprotobuf)
@@ -83,7 +85,64 @@ services.AddSlimMessageBus(mbb =>
8385
});
8486
```
8587

86-
By default the plugin adds a custom converter (see [`ObjectToInferredTypesConverter`](../src/SlimMessageBus.Host.Serialization.SystemTextJson/ObjectToInferredTypesConverter.cs)) that infers primitive types whenever the type to deseriaize is object (unknown). This helps with header value serialization for transport providers that transmit the headers as binary (Kafka). See the source code for better explanation.
88+
### Polymorphic Type Handling
89+
90+
By default, the serializer uses the `useActualTypeOnSerialize: true` parameter, which ensures that the actual runtime type of the message is used during serialization. This is important for polymorphic message types where you might publish a derived type but declare the producer with a base type.
91+
92+
```cs
93+
// Default behavior - uses actual type during serialization
94+
mbb.AddJsonSerializer(useActualTypeOnSerialize: true);
95+
96+
// Alternative - uses declared type during serialization
97+
mbb.AddJsonSerializer(useActualTypeOnSerialize: false);
98+
```
99+
100+
**When `useActualTypeOnSerialize: true` (default):**
101+
102+
- The serializer uses `message.GetType()` to determine which properties to serialize
103+
- All properties of the derived type are included in the JSON
104+
- Useful when consumers know the actual message type and need all properties
105+
- Works without requiring `[JsonDerivedType]` attributes on base message types
106+
107+
**When `useActualTypeOnSerialize: false`:**
108+
109+
- The serializer uses the declared `messageType` parameter passed to the serialize method
110+
- Only properties of the declared base type are serialized
111+
- Useful for strict contract enforcement where consumers should only see base type properties
112+
- Requires `[JsonDerivedType]` attributes on base types if you need polymorphic deserialization
113+
114+
**Example:**
115+
116+
```cs
117+
public class BaseMessage
118+
{
119+
public string Id { get; set; }
120+
}
121+
122+
public class DerivedMessage : BaseMessage
123+
{
124+
public string ExtraData { get; set; }
125+
}
126+
127+
// Producer configuration
128+
mbb.Produce<BaseMessage>(x => x.DefaultTopic("my-topic"));
129+
130+
// When publishing
131+
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
132+
await bus.Publish(message);
133+
134+
// With useActualTypeOnSerialize: true (default)
135+
// Serialized JSON: {"id":"123","extraData":"test"}
136+
137+
// With useActualTypeOnSerialize: false
138+
// Serialized JSON: {"id":"123"}
139+
```
140+
141+
> **Note:** System.Text.Json requires `[JsonDerivedType]` attributes on the base type for proper polymorphic deserialization. The `useActualTypeOnSerialize: true` setting helps with serialization but doesn't affect deserialization requirements.
142+
143+
### Object to Inferred Types Converter
144+
145+
By default the plugin adds a custom converter (see [`ObjectToInferredTypesConverter`](../src/SlimMessageBus.Host.Serialization.SystemTextJson/ObjectToInferredTypesConverter.cs)) that infers primitive types whenever the type to deserialize is object (unknown). This helps with header value serialization for transport providers that transmit the headers as binary (Kafka). See the source code for better explanation.
87146

88147
## Json (Newtonsoft.Json)
89148

@@ -110,6 +169,8 @@ var jsonSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
110169
mbb.AddJsonSerializer(jsonSerializerSettings, Encoding.UTF8);
111170
```
112171

172+
> **Note:** Newtonsoft.Json always serializes using the declared type (`messageType` parameter). For polymorphic type handling with Newtonsoft.Json, use the `TypeNameHandling` setting in `JsonSerializerSettings`.
173+
113174
## Avro
114175

115176
Nuget package: [SlimMessageBus.Host.Serialization.Avro](https://www.nuget.org/packages/SlimMessageBus.Host.Serialization.Avro)

src/Host.Plugin.Properties.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Import Project="Common.NuGet.Properties.xml" />
55

66
<PropertyGroup>
7-
<Version>3.3.7-rc100</Version>
7+
<Version>3.3.7-rc200</Version>
88
</PropertyGroup>
99

1010
</Project>

src/SlimMessageBus.Host.Serialization.SystemTextJson/JsonMessageSerializer.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
/// Implementation of <see cref="IMessageSerializer"/> using <see cref="JsonSerializer"/>.
99
/// </summary>
1010
public class JsonMessageSerializer : IMessageSerializer, IMessageSerializer<string>, IMessageSerializerProvider
11-
{
11+
{
12+
private readonly bool _passActualTypeOnSerialize;
13+
1214
/// <summary>
1315
/// <see cref="JsonSerializerOptions"/> options for the JSON serializer. By default adds <see cref="ObjectToInferredTypesConverter"/> converter.
1416
/// </summary>
1517
public JsonSerializerOptions Options { get; set; }
1618

17-
public JsonMessageSerializer(JsonSerializerOptions options = null)
19+
public JsonMessageSerializer(JsonSerializerOptions options = null, bool useActualTypeOnSerialize = true)
1820
{
1921
Options = options ?? CreateDefaultOptions();
22+
_passActualTypeOnSerialize = useActualTypeOnSerialize;
2023
}
2124

2225
public virtual JsonSerializerOptions CreateDefaultOptions()
@@ -34,7 +37,7 @@ public virtual JsonSerializerOptions CreateDefaultOptions()
3437
#region Implementation of IMessageSerializer
3538

3639
public byte[] Serialize(Type messageType, IDictionary<string, object> headers, object message, object transportMessage)
37-
=> JsonSerializer.SerializeToUtf8Bytes(message, messageType, Options);
40+
=> JsonSerializer.SerializeToUtf8Bytes(message, _passActualTypeOnSerialize ? message.GetType() : messageType, Options);
3841

3942
public object Deserialize(Type messageType, IReadOnlyDictionary<string, object> headers, byte[] payload, object transportMessage)
4043
=> JsonSerializer.Deserialize(payload, messageType, Options)!;
@@ -44,7 +47,7 @@ public object Deserialize(Type messageType, IReadOnlyDictionary<string, object>
4447
#region Implementation of IMessageSerializer<string>
4548

4649
string IMessageSerializer<string>.Serialize(Type messageType, IDictionary<string, object> headers, object message, object transportMessage)
47-
=> JsonSerializer.Serialize(message, messageType, Options);
50+
=> JsonSerializer.Serialize(message, _passActualTypeOnSerialize ? message.GetType() : messageType, Options);
4851

4952
public object Deserialize(Type messageType, IReadOnlyDictionary<string, object> headers, string payload, object transportMessage)
5053
=> JsonSerializer.Deserialize(payload, messageType, Options)!;

src/SlimMessageBus.Host.Serialization.SystemTextJson/SerializationBuilderExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ public static class SerializationBuilderExtensions
1313
/// Registers the <see cref="IMessageSerializer"/> with implementation as <see cref="JsonMessageSerializer"/>.
1414
/// </summary>
1515
/// <param name="builder"></param>
16+
/// <param name="options"></param>
17+
/// <param name="useActualTypeOnSerialize">Should the actual message type be used during serialization? If false, the producer declared type will be passed (e.g. base type of the actual message). It will make a difference for polymorphic types.</param>
1618
/// <returns></returns>
17-
public static TBuilder AddJsonSerializer<TBuilder>(this TBuilder builder, JsonSerializerOptions options = null)
19+
public static TBuilder AddJsonSerializer<TBuilder>(this TBuilder builder, JsonSerializerOptions options = null, bool useActualTypeOnSerialize = true)
1820
where TBuilder : ISerializationBuilder
1921
{
2022
builder.RegisterSerializer<JsonMessageSerializer>(services =>
2123
{
2224
// Add the implementation
23-
services.TryAddSingleton(svp => new JsonMessageSerializer(options ?? svp.GetService<JsonSerializerOptions>()));
25+
services.TryAddSingleton(svp => new JsonMessageSerializer(options ?? svp.GetService<JsonSerializerOptions>(), useActualTypeOnSerialize));
2426
// Add the serializer as IMessageSerializer<string>
2527
services.TryAddSingleton(svp => svp.GetRequiredService<JsonMessageSerializer>() as IMessageSerializer<string>);
2628
});

src/Tests/SlimMessageBus.Host.Serialization.SystemTextJson.Test/JsonMessageSerializerTests.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,106 @@ public void When_RegisterSerializer_Then_UsesOptionsFromContainerIfAvailable(boo
8787
// assert
8888
serviceProvider.GetService<IMessageSerializerProvider>().Should().BeSameAs(subject);
8989
}
90+
91+
// Test types for polymorphic serialization
92+
public class BaseMessage
93+
{
94+
public string Id { get; set; }
95+
}
96+
97+
public class DerivedMessage : BaseMessage
98+
{
99+
public string ExtraData { get; set; }
100+
}
101+
102+
[Theory]
103+
[InlineData(true, true)] // useActualTypeOnSerialize: true, shouldContainExtraData: true
104+
[InlineData(false, false)] // useActualTypeOnSerialize: false, shouldContainExtraData: false
105+
public void When_SerializePolymorphicType_Given_UseActualTypeOnSerialize_Then_SerializesAccordingly(bool useActualTypeOnSerialize, bool shouldContainExtraData)
106+
{
107+
// arrange
108+
var subject = new JsonMessageSerializer(useActualTypeOnSerialize: useActualTypeOnSerialize);
109+
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
110+
111+
// act
112+
var bytes = subject.Serialize(typeof(BaseMessage), null, message, null);
113+
var json = System.Text.Encoding.UTF8.GetString(bytes);
114+
115+
// assert
116+
json.Should().Contain("id");
117+
118+
if (shouldContainExtraData)
119+
{
120+
json.Should().Contain("extraData");
121+
json.Should().Contain("test");
122+
}
123+
else
124+
{
125+
json.Should().NotContain("extraData");
126+
}
127+
}
128+
129+
[Theory]
130+
[InlineData(true, true)] // useActualTypeOnSerialize: true, shouldContainExtraData: true
131+
[InlineData(false, false)] // useActualTypeOnSerialize: false, shouldContainExtraData: false
132+
public void When_SerializePolymorphicTypeString_Given_UseActualTypeOnSerialize_Then_SerializesAccordingly(bool useActualTypeOnSerialize, bool shouldContainExtraData)
133+
{
134+
// arrange
135+
var subject = new JsonMessageSerializer(useActualTypeOnSerialize: useActualTypeOnSerialize) as IMessageSerializer<string>;
136+
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
137+
138+
// act
139+
var json = subject.Serialize(typeof(BaseMessage), null, message, null);
140+
141+
// assert
142+
json.Should().Contain("id");
143+
144+
if (shouldContainExtraData)
145+
{
146+
json.Should().Contain("extraData");
147+
json.Should().Contain("test");
148+
}
149+
else
150+
{
151+
json.Should().NotContain("extraData");
152+
}
153+
}
154+
155+
[Fact]
156+
public void When_UseActualTypeOnSerializeIsTrue_Then_RoundTripWithActualTypeSucceeds()
157+
{
158+
// arrange
159+
var subject = new JsonMessageSerializer(useActualTypeOnSerialize: true);
160+
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
161+
162+
// act - serialize with actual type
163+
var bytes = subject.Serialize(typeof(BaseMessage), null, message, null);
164+
165+
// To deserialize back to DerivedMessage, we need to deserialize to DerivedMessage type
166+
var deserializedMessage = subject.Deserialize(typeof(DerivedMessage), null, bytes, null) as DerivedMessage;
167+
168+
// assert
169+
deserializedMessage.Should().NotBeNull();
170+
deserializedMessage.Id.Should().Be("123");
171+
deserializedMessage.ExtraData.Should().Be("test");
172+
}
173+
174+
[Fact]
175+
public void When_UseActualTypeOnSerializeIsFalse_Then_RoundTripWithBaseTypeSucceeds()
176+
{
177+
// arrange
178+
var subject = new JsonMessageSerializer(useActualTypeOnSerialize: false);
179+
var message = new DerivedMessage { Id = "123", ExtraData = "test" };
180+
181+
// act - serialize with declared type (BaseMessage)
182+
var bytes = subject.Serialize(typeof(BaseMessage), null, message, null);
183+
184+
// Deserialize back to BaseMessage
185+
var deserializedMessage = subject.Deserialize(typeof(BaseMessage), null, bytes, null) as BaseMessage;
186+
187+
// assert - only base properties preserved
188+
deserializedMessage.Should().NotBeNull();
189+
deserializedMessage.Id.Should().Be("123");
190+
deserializedMessage.Should().BeOfType<BaseMessage>();
191+
}
90192
}

0 commit comments

Comments
 (0)