From 40ac844bf5738687f12dc649761b13a2ca8faa8f Mon Sep 17 00:00:00 2001
From: Pedro Cavaleiro
Date: Tue, 17 Feb 2026 14:51:34 +0000
Subject: [PATCH 1/7] Fixes StreamLabels.Underlying message
---
.../DataTypes/StreamlabelsUnderlyingMessageData.cs | 2 +-
.../StreamlabelsUnderlyingMessageDonationGoal.cs | 14 ++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
index d8900a1..c11abce 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
@@ -7,7 +7,7 @@ namespace Streamlabs.SocketClient.Messages.DataTypes;
public sealed record StreamlabelsUnderlyingMessageData : IHasMessageId, IHasPriority
{
[JsonPropertyName("donation_goal")]
- public required string DonationGoal { get; init; }
+ public required StreamlabelsUnderlyingMessageDonationGoal DonationGoal { get; init; }
[JsonPropertyName("most_recent_donator")]
public required Donator MostRecentDonator { get; init; }
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
new file mode 100644
index 0000000..48ae8ae
--- /dev/null
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Streamlabs.SocketClient.Messages.DataTypes;
+
+public sealed record StreamlabelsUnderlyingMessageDonationGoal {
+ [JsonPropertyName("title")]
+ public required string Title { get; init; }
+
+ [JsonPropertyName("currentAmount")]
+ public required string CurrentAmount { get; init; }
+
+ [JsonPropertyName("goalAmount")]
+ public required string GoalAmount { get; init; }
+}
\ No newline at end of file
From 1fbc54722c6e41e86d13b399722a3692ea546c9b Mon Sep 17 00:00:00 2001
From: Pedro Cavaleiro
Date: Tue, 17 Feb 2026 14:54:31 +0000
Subject: [PATCH 2/7] Fixes StreamLabelsMessageData
---
.../Messages/DataTypes/StreamlabelsMessageData.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
index c6d963c..53325be 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
@@ -138,7 +138,7 @@ public sealed record StreamlabelsMessageData : IHasMessageId, IHasPriority
public required string SessionMostRecentMonthlyDonator { get; init; }
[JsonPropertyName("cloudbot_counter_deaths")]
- public required string CloudbotCounterDeaths { get; init; }
+ public string CloudbotCounterDeaths { get; init; } // this property might not be present
[JsonPropertyName("monthly_subscriber_count")]
[JsonConverter(typeof(IntStringConverter))]
From 4be9f886f54e5ccfbae1455c7dd9101a1a53325f Mon Sep 17 00:00:00 2001
From: Pedro Cavaleiro
Date: Tue, 17 Feb 2026 15:33:06 +0000
Subject: [PATCH 3/7] Makes the property CloudbotCounterDeaths nullable
---
.../Messages/DataTypes/StreamlabelsMessageData.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
index 53325be..afa3663 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
@@ -138,7 +138,7 @@ public sealed record StreamlabelsMessageData : IHasMessageId, IHasPriority
public required string SessionMostRecentMonthlyDonator { get; init; }
[JsonPropertyName("cloudbot_counter_deaths")]
- public string CloudbotCounterDeaths { get; init; } // this property might not be present
+ public string? CloudbotCounterDeaths { get; init; } // this property might not be present
[JsonPropertyName("monthly_subscriber_count")]
[JsonConverter(typeof(IntStringConverter))]
From 8facada1e58cacc1122ab680eedb9435d5764e99 Mon Sep 17 00:00:00 2001
From: Pedro Cavaleiro
Date: Sat, 21 Feb 2026 15:44:33 +0000
Subject: [PATCH 4/7] Improves Streamlabs message deserialization
Introduces a `FlexibleObjectConverter` to handle varying JSON input types for properties within Streamlabs messages. This converter allows deserialization from direct JSON objects/arrays, or from double-encoded JSON strings, preventing errors due to API inconsistencies.
Applies the `FlexibleObjectConverter` to properties in `StreamlabelsUnderlyingMessageData` that exhibit this flexible behavior, such as donator objects and lists, making them nullable to accommodate unparseable string values.
Adds a new test case to validate the robust deserialization of these flexible JSON structures.
---
.../Converters/FlexibleObjectConverter.cs | 57 +
.../StreamlabelsUnderlyingMessageData.cs | 38 +-
.../MessageJson/streamlabelsUnderlying3.json | 1329 +++++++++++++++++
.../MessageTypeTests.cs | 1 +
4 files changed, 1412 insertions(+), 13 deletions(-)
create mode 100644 src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
create mode 100644 test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json
diff --git a/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs b/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
new file mode 100644
index 0000000..33acd44
--- /dev/null
+++ b/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
@@ -0,0 +1,57 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Streamlabs.SocketClient.Converters;
+
+///
+/// Provides a flexible JSON converter for that handles mixed input types.
+/// This converter can deserialize from a standard JSON object,
+/// an escaped JSON string (double-encoded), or gracefully handle plain non-JSON strings by returning null.
+///
+/// The reference type to deserialize into.
+public class FlexibleObjectConverter : JsonConverter
+ where T : class
+{
+ public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.StartObject or JsonTokenType.StartArray:
+ return JsonSerializer.Deserialize(ref reader, options);
+ case JsonTokenType.String:
+ {
+ string? value = reader.GetString();
+ if (value == null)
+ {
+ return null;
+ }
+
+ string trimmed = value.Trim();
+
+ bool isJsonObject = trimmed.Length > 0 && trimmed[0] == '{' && trimmed[^1] == '}';
+ bool isJsonArray = trimmed.Length > 0 && trimmed[0] == '[' && trimmed[^1] == ']';
+
+ if (isJsonObject || isJsonArray)
+ {
+ try
+ {
+ return JsonSerializer.Deserialize(trimmed, options);
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, options);
+ }
+}
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
index c11abce..508c4a3 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
@@ -7,16 +7,19 @@ namespace Streamlabs.SocketClient.Messages.DataTypes;
public sealed record StreamlabelsUnderlyingMessageData : IHasMessageId, IHasPriority
{
[JsonPropertyName("donation_goal")]
- public required StreamlabelsUnderlyingMessageDonationGoal DonationGoal { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public StreamlabelsUnderlyingMessageDonationGoal? DonationGoal { get; init; }
[JsonPropertyName("most_recent_donator")]
public required Donator MostRecentDonator { get; init; }
[JsonPropertyName("session_most_recent_donator")]
- public required string SessionMostRecentDonator { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public Donator? SessionMostRecentDonator { get; init; }
[JsonPropertyName("session_donators")]
- public required string SessionDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? SessionDonators { get; init; }
[JsonPropertyName("total_donation_amount")]
public required DonationAmount TotalDonationAmount { get; init; }
@@ -37,31 +40,40 @@ public sealed record StreamlabelsUnderlyingMessageData : IHasMessageId, IHasPrio
public required TopDonator AllTimeTopDonator { get; init; }
[JsonPropertyName("monthly_top_donator")]
- public required string MonthlyTopDonator { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public TopDonator? MonthlyTopDonator { get; init; }
[JsonPropertyName("weekly_top_donator")]
- public required string WeeklyTopDonator { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public TopDonator? WeeklyTopDonator { get; init; }
[JsonPropertyName("30day_top_donator")]
- public required string ThirtyDayTopDonator { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public TopDonator? ThirtyDayTopDonator { get; init; }
[JsonPropertyName("session_top_donator")]
- public required string SessionTopDonator { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public TopDonator? SessionTopDonator { get; init; }
[JsonPropertyName("all_time_top_donators")]
- public required IReadOnlyCollection AllTimeTopDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? AllTimeTopDonators { get; init; }
[JsonPropertyName("monthly_top_donators")]
- public required string MonthlyTopDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? MonthlyTopDonators { get; init; }
[JsonPropertyName("weekly_top_donators")]
- public required string WeeklyTopDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? WeeklyTopDonators { get; init; }
[JsonPropertyName("30day_top_donators")]
- public required string ThirtyDayTopDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? ThirtyDayTopDonators { get; init; }
[JsonPropertyName("session_top_donators")]
- public required string SessionTopDonators { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter>))]
+ public IReadOnlyCollection? SessionTopDonators { get; init; }
[JsonPropertyName("all_time_top_donations")]
public required IReadOnlyCollection AllTimeTopDonations { get; init; }
@@ -133,7 +145,7 @@ public sealed record StreamlabelsUnderlyingMessageData : IHasMessageId, IHasPrio
public required string SessionMostRecentMonthlyDonator { get; init; }
[JsonPropertyName("cloudbot_counter_deaths")]
- public required Counter CloudbotCounterDeaths { get; init; }
+ public Counter? CloudbotCounterDeaths { get; init; }
[JsonPropertyName("monthly_subscriber_count")]
public required Count MonthlySubscriberCount { get; init; }
diff --git a/test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json b/test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json
new file mode 100644
index 0000000..3d7de1f
--- /dev/null
+++ b/test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json
@@ -0,0 +1,1329 @@
+[
+ {
+ "type": "streamlabels.underlying",
+ "message": {
+ "hash": "d6b35f97e11c4926503ec48c63bffc56",
+ "data": {
+ "donation_goal": {
+ "title": "Stream Goal!",
+ "currentAmount": "$170.00",
+ "goalAmount": "$200.00"
+ },
+ "most_recent_donator": {
+ "name": "donTIsingTO",
+ "amount": "$10.00",
+ "message": "So we end up on a good note\nhttps://youtu.be/r6L-GUOAhGo?si=y9hv0B2I53XgM63w",
+ "created_at": "2026-02-09 23:53:14"
+ },
+ "session_most_recent_donator": {
+ "name": "donTIsingTO",
+ "amount": "$10.00",
+ "message": "So we end up on a good note\nhttps://youtu.be/r6L-GUOAhGo?si=y9hv0B2I53XgM63w",
+ "created_at": "2026-02-09 23:53:14"
+ },
+ "session_donators": [
+ {
+ "name": "donTIsingTO",
+ "amount": "$10.00",
+ "message": "So we end up on a good note\nhttps://youtu.be/r6L-GUOAhGo?si=y9hv0B2I53XgM63w",
+ "created_at": "2026-02-09 23:53:14"
+ },
+ {
+ "name": "Dieuctunantifa",
+ "amount": "$10.00",
+ "message": "If you have fan of Primus listen to this... Is a band from Quebec Canada sorry they have mask...\nhttps://youtu.be/t7OIc-DBRXM?si=to78DUB-T7G2SdoB",
+ "created_at": "2026-02-09 23:14:47"
+ },
+ {
+ "name": "donTIsingTO",
+ "amount": "$10.00",
+ "message": "Eeey! still in my old job\nhttps://youtu.be/QPoHXvYE7-0?si=xzv_op__B-nrQcf1",
+ "created_at": "2026-02-09 23:07:03"
+ },
+ {
+ "name": "HitmanptZ",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=4F0vGVu1xlc",
+ "created_at": "2026-02-09 22:30:56"
+ },
+ {
+ "name": "halfgodhalfdevil",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=jH1cyyae2ws",
+ "created_at": "2026-02-09 22:25:32"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "https://youtu.be/DvD8y1IdYgs",
+ "created_at": "2026-02-09 22:21:13"
+ },
+ {
+ "name": "lostpoetstories",
+ "amount": "$10.00",
+ "message": "https://youtu.be/EHhRpWF6wi4",
+ "created_at": "2026-02-09 22:20:55"
+ },
+ {
+ "name": "livingwave17",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=LvkyAYIaLdQ",
+ "created_at": "2026-02-09 21:59:16"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=oCLPIAUJlSA&list=PLD-NPUxVuvUo37bNo9aWTyM14pxVw7Vfl&index=178",
+ "created_at": "2026-02-09 21:49:24"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=4cW0qtMFKs0",
+ "created_at": "2026-02-09 21:47:41"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=b1Mw5VwDvPQ",
+ "created_at": "2026-02-09 21:47:35"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "https://youtu.be/h5xRjHwH0xg",
+ "created_at": "2026-02-09 21:42:30"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$5.00",
+ "message": "5 on top of the other 25 for 3 requests\n\nhttps://youtu.be/IJgCvnvO3Bc?si=dmR1xHPTImFUXWmy\n\nhttps://youtu.be/waQ9BKMacII?si=EVDQ94GeA9oAGOxV\n\nhttps://youtu.be/VoSjxKBT3CA?si=SVshHywnYLuE5eH1",
+ "created_at": "2026-02-09 21:30:24"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-02-09 21:24:22"
+ },
+ {
+ "name": "baldmikes",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=rv-4AliPi6Q",
+ "created_at": "2026-02-09 21:21:56"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lez go!",
+ "created_at": "2026-02-09 21:20:10"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27",
+ "message": "Phew. Glad I caught you before you ended stream. :)",
+ "created_at": "2026-02-02 23:54:48"
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-02-02 23:16:18"
+ },
+ {
+ "name": "BR",
+ "amount": "$10.00",
+ "message": "Yee haw",
+ "created_at": "2026-02-02 22:43:23"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-02-02 21:58:34"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-02-02 21:39:49"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-02-02 21:34:40"
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00",
+ "message": "If you get shot and run to the cop, you not like V\nYou ain't got no work on the block, you not like V\nIt's hot, you ain't got no drop, you not like V",
+ "created_at": "2026-02-02 21:20:48"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lets gooooo",
+ "created_at": "2026-02-02 21:13:39"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "",
+ "created_at": "2026-01-26 21:32:21"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$20.00",
+ "message": "",
+ "created_at": "2026-01-26 21:26:57"
+ }
+ ],
+ "total_donation_amount": {
+ "amount": "$66,678.93"
+ },
+ "monthly_donation_amount": {
+ "amount": "$313.27"
+ },
+ "weekly_donation_amount": {
+ "amount": "$0.00"
+ },
+ "30day_donation_amount": {
+ "amount": "$393.27"
+ },
+ "session_donation_amount": {
+ "amount": "$393.27"
+ },
+ "all_time_top_donator": {
+ "name": "EOYVickyStream",
+ "amount": "$11,304.00"
+ },
+ "monthly_top_donator": {
+ "name": "SchilluminumFoil",
+ "amount": "$55.00"
+ },
+ "weekly_top_donator": "",
+ "30day_top_donator": {
+ "name": "SchilluminumFoil",
+ "amount": "$65.00"
+ },
+ "session_top_donator": {
+ "name": "SchilluminumFoil",
+ "amount": "$65.00"
+ },
+ "all_time_top_donators": [
+ {
+ "name": "EOYVickyStream",
+ "amount": "$11,304.00"
+ },
+ {
+ "name": "Guy_in_kitchen",
+ "amount": "$6,071.28"
+ },
+ {
+ "name": "SaBiN666999",
+ "amount": "$4,910.75"
+ },
+ {
+ "name": "MatthewDyffryn",
+ "amount": "$3,250.55"
+ },
+ {
+ "name": "chains",
+ "amount": "$2,237.58"
+ },
+ {
+ "name": "philipk170",
+ "amount": "$2,221.81"
+ },
+ {
+ "name": "Thereceptor1",
+ "amount": "$2,100.58"
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": "$2,076.00"
+ },
+ {
+ "name": "RLC1389",
+ "amount": "$1,621.00"
+ },
+ {
+ "name": "TaverenTech",
+ "amount": "$1,385.00"
+ }
+ ],
+ "monthly_top_donators": [
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$55.00"
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00"
+ },
+ {
+ "name": "donTIsingTO",
+ "amount": "$20.00"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$20.00"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$20.00"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$20.00"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$20.00"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27"
+ },
+ {
+ "name": "Dieuctunantifa",
+ "amount": "$10.00"
+ },
+ {
+ "name": "HitmanptZ",
+ "amount": "$10.00"
+ }
+ ],
+ "weekly_top_donators": "",
+ "30day_top_donators": [
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$65.00"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$50.00"
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00"
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$30.00"
+ },
+ {
+ "name": "donTIsingTO",
+ "amount": "$20.00"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$20.00"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$20.00"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$20.00"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27"
+ },
+ {
+ "name": "Dieuctunantifa",
+ "amount": "$10.00"
+ }
+ ],
+ "session_top_donators": [
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$65.00"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$50.00"
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00"
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$30.00"
+ },
+ {
+ "name": "donTIsingTO",
+ "amount": "$20.00"
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$20.00"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$20.00"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$20.00"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27"
+ },
+ {
+ "name": "Dieuctunantifa",
+ "amount": "$10.00"
+ }
+ ],
+ "all_time_top_donations": [
+ {
+ "name": "chainsofthought",
+ "amount": "$2,056.00",
+ "message": "From your Patrons. We wish only the best for you, and love your work."
+ },
+ {
+ "name": "vickypsarakis",
+ "amount": "$2,000.00",
+ "message": ""
+ },
+ {
+ "name": "TaverenTech",
+ "amount": "$1,000.00",
+ "message": "I hereby sponsor one whole song, preferably the heaviest and/or darkest on of the next LP that you will do on tour in 2024!~"
+ },
+ {
+ "name": "vikingdude78",
+ "amount": "$1,000.00",
+ "message": "Have some money. Thanks for putting up with me. Lots of love! - Jan"
+ },
+ {
+ "name": "KeyboardVicky",
+ "amount": "$1,000.00",
+ "message": "I’m so good at what i do I would be ashamed if I admitted it."
+ },
+ {
+ "name": "Oreo",
+ "amount": "$1,000.00",
+ "message": "Yes mom. You earned it."
+ },
+ {
+ "name": "Vickypsarakis",
+ "amount": "$1,000.00",
+ "message": ""
+ },
+ {
+ "name": "Pedrocavaleiro",
+ "amount": "$1,000.00",
+ "message": ""
+ },
+ {
+ "name": "vickypsarakis",
+ "amount": "$1,000.00",
+ "message": ""
+ },
+ {
+ "name": "vickypsarakis",
+ "amount": "$950.00",
+ "message": ""
+ }
+ ],
+ "monthly_top_donations": [
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00",
+ "message": "If you get shot and run to the cop, you not like V\nYou ain't got no work on the block, you not like V\nIt's hot, you ain't got no drop, you not like V"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lez go!"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lets gooooo"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27",
+ "message": "Phew. Glad I caught you before you ended stream. :)"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "https://youtu.be/DvD8y1IdYgs"
+ },
+ {
+ "name": "livingwave17",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=LvkyAYIaLdQ"
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$10.00",
+ "message": ""
+ },
+ {
+ "name": "metaltvtwitch",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=b1Mw5VwDvPQ"
+ },
+ {
+ "name": "N0ko_WOT",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=4cW0qtMFKs0"
+ },
+ {
+ "name": "x_x_madmaxx",
+ "amount": "$10.00",
+ "message": ""
+ }
+ ],
+ "weekly_top_donations": [],
+ "30day_top_donations": [
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00",
+ "message": "If you get shot and run to the cop, you not like V\nYou ain't got no work on the block, you not like V\nIt's hot, you ain't got no drop, you not like V"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lez go!"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lets gooooo"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$20.00",
+ "message": ""
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$20.00",
+ "message": "Take this for your morning coffee!"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27",
+ "message": "Phew. Glad I caught you before you ended stream. :)"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": ""
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "https://youtu.be/DvD8y1IdYgs"
+ },
+ {
+ "name": "livingwave17",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=LvkyAYIaLdQ"
+ },
+ {
+ "name": "craigstanley",
+ "amount": "$10.00",
+ "message": "For The Better - When Darkness Falls"
+ }
+ ],
+ "session_top_donations": [
+ {
+ "name": "QuietStorm90",
+ "amount": "$50.00",
+ "message": "If you get shot and run to the cop, you not like V\nYou ain't got no work on the block, you not like V\nIt's hot, you ain't got no drop, you not like V"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lez go!"
+ },
+ {
+ "name": "SchilluminumFoil",
+ "amount": "$25.00",
+ "message": "Lets gooooo"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$20.00",
+ "message": ""
+ },
+ {
+ "name": "andreidan37",
+ "amount": "$20.00",
+ "message": "Take this for your morning coffee!"
+ },
+ {
+ "name": "Necroblitz",
+ "amount": "$18.27",
+ "message": "Phew. Glad I caught you before you ended stream. :)"
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": ""
+ },
+ {
+ "name": "dpxcz",
+ "amount": "$10.00",
+ "message": "https://youtu.be/DvD8y1IdYgs"
+ },
+ {
+ "name": "livingwave17",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=LvkyAYIaLdQ"
+ },
+ {
+ "name": "craigstanley",
+ "amount": "$10.00",
+ "message": "For The Better - When Darkness Falls"
+ }
+ ],
+ "all_time_top_monthly_donator": {
+ "name": "OGmagZz",
+ "amount": "$10.00"
+ },
+ "monthly_top_monthly_donator": "",
+ "weekly_top_monthly_donator": "",
+ "30day_top_monthly_donator": "",
+ "session_top_monthly_donator": "",
+ "all_time_top_monthly_donators": [
+ {
+ "name": "OGmagZz",
+ "amount": "$10.00"
+ }
+ ],
+ "monthly_top_monthly_donators": "",
+ "weekly_top_monthly_donators": "",
+ "30day_top_monthly_donators": "",
+ "session_top_monthly_donators": "",
+ "total_monthly_donator_count": {
+ "count": "0"
+ },
+ "monthly_monthly_donator_count": {
+ "count": "0"
+ },
+ "weekly_monthly_donator_count": {
+ "count": "0"
+ },
+ "30day_monthly_donator_count": {
+ "count": "0"
+ },
+ "session_monthly_donator_count": {
+ "count": "0"
+ },
+ "most_recent_monthly_donator": {
+ "name": "OGmagZz",
+ "amount": "$10.00",
+ "message": "https://www.youtube.com/watch?v=iJzDaKixsWg",
+ "created_at": "2023-10-02 20:04:46"
+ },
+ "session_monthly_donators": "",
+ "session_most_recent_monthly_donator": "",
+ "monthly_subscriber_count": {
+ "count": "11"
+ },
+ "weekly_subscriber_count": {
+ "count": "1"
+ },
+ "30day_subscriber_count": {
+ "count": "17"
+ },
+ "session_subscriber_count": {
+ "count": "17"
+ },
+ "session_follower_count": {
+ "count": "51"
+ },
+ "session_most_recent_follower": {
+ "name": "heavymetalnerds"
+ },
+ "session_most_recent_subscriber": {
+ "name": "austros99",
+ "months": 1
+ },
+ "session_most_recent_resubscriber": {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ "session_subscribers": [
+ {
+ "name": "austros99",
+ "months": 1
+ },
+ {
+ "name": "LostBandit",
+ "months": 1
+ },
+ {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ {
+ "name": "metaltvtwitch",
+ "months": 48
+ },
+ {
+ "name": "niksthekicks",
+ "months": 61
+ },
+ {
+ "name": "Tim_Volkihar",
+ "months": 55
+ },
+ {
+ "name": "DefinitelyNotBR",
+ "months": 34
+ },
+ {
+ "name": "mis0_13",
+ "months": 23
+ },
+ {
+ "name": "tokyoraven83",
+ "months": 1
+ },
+ {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ {
+ "name": "tonimackerz",
+ "months": 51
+ },
+ {
+ "name": "GiannisSatanas",
+ "months": 57
+ },
+ {
+ "name": "Radio_Hatice",
+ "months": 55
+ },
+ {
+ "name": "EtilenoMiope",
+ "months": 46
+ },
+ {
+ "name": "HitmanptZ",
+ "months": 60
+ },
+ {
+ "name": "jokhsg_",
+ "months": 20
+ },
+ {
+ "name": "tonimackerz",
+ "months": 50
+ }
+ ],
+ "session_followers": [
+ {
+ "name": "heavymetalnerds"
+ },
+ {
+ "name": "lewiesell6605"
+ },
+ {
+ "name": "jeffreyvee"
+ },
+ {
+ "name": "Borghil_"
+ },
+ {
+ "name": "Onyxine"
+ },
+ {
+ "name": "MisterFribbles"
+ },
+ {
+ "name": "dieuctunantifa"
+ },
+ {
+ "name": "thatdudedaniel_"
+ },
+ {
+ "name": "coelhinhopistola"
+ },
+ {
+ "name": "modestmao"
+ },
+ {
+ "name": "walleeezz"
+ },
+ {
+ "name": "neil_blas"
+ },
+ {
+ "name": "watfordred"
+ },
+ {
+ "name": "Nikkeeeew"
+ },
+ {
+ "name": "ronelracing"
+ },
+ {
+ "name": "HeelMatt06"
+ },
+ {
+ "name": "TheInnocent040"
+ },
+ {
+ "name": "Golihat"
+ },
+ {
+ "name": "halfgodhalfdevil666"
+ },
+ {
+ "name": "plantewhore"
+ },
+ {
+ "name": "Neshura"
+ },
+ {
+ "name": "CulannsHound"
+ },
+ {
+ "name": "borisbuch84"
+ },
+ {
+ "name": "Xzylez"
+ },
+ {
+ "name": "DashKetchumGaming"
+ },
+ {
+ "name": "dgorj34"
+ },
+ {
+ "name": "deucalion289"
+ },
+ {
+ "name": "mrmiiata"
+ },
+ {
+ "name": "BlearyLine7"
+ },
+ {
+ "name": "eid_al_wriman"
+ },
+ {
+ "name": "Eberhauer"
+ },
+ {
+ "name": "grafforever"
+ },
+ {
+ "name": "Sjelefred_"
+ },
+ {
+ "name": "envoy0815"
+ },
+ {
+ "name": "Linesmilefjees"
+ },
+ {
+ "name": "93veronika"
+ },
+ {
+ "name": "boristhekrockodilian"
+ },
+ {
+ "name": "aekoenic"
+ },
+ {
+ "name": "LailaMelodie"
+ },
+ {
+ "name": "PyreBlight_08"
+ },
+ {
+ "name": "tokyoraven83"
+ },
+ {
+ "name": "613sFinest"
+ },
+ {
+ "name": "syst3t1c"
+ },
+ {
+ "name": "PurplProto"
+ },
+ {
+ "name": "hellbringer788"
+ },
+ {
+ "name": "jesse_541"
+ },
+ {
+ "name": "Lawnce"
+ },
+ {
+ "name": "MuchaMagia"
+ },
+ {
+ "name": "catladyniki"
+ },
+ {
+ "name": "annalininred23"
+ },
+ {
+ "name": "LostBandit"
+ }
+ ],
+ "most_recent_follower": {
+ "name": "heavymetalnerds"
+ },
+ "most_recent_subscriber": {
+ "name": "austros99",
+ "months": 1
+ },
+ "most_recent_resubscriber": {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ "total_follower_count": {
+ "count": "10,229"
+ },
+ "total_subscriber_count": {
+ "count": "49"
+ },
+ "total_subscriber_score": {
+ "count": "61"
+ },
+ "monthly_subscriber_score": {
+ "count": "3"
+ },
+ "weekly_subscriber_score": {
+ "count": "1"
+ },
+ "30day_subscriber_score": {
+ "count": "3"
+ },
+ "session_subscriber_score": {
+ "count": "3"
+ },
+ "most_recent_cheerer": {
+ "name": "joXuahardylee",
+ "amount": 10,
+ "message": "ShowLove10 how are you Vicky? ??? I'm Happy to see you again"
+ },
+ "session_most_recent_cheerer": {
+ "name": "joXuahardylee",
+ "amount": 10,
+ "message": null
+ },
+ "session_cheerers": [
+ {
+ "name": "joXuahardylee",
+ "amount": 10,
+ "message": null
+ }
+ ],
+ "total_cheer_amount": {
+ "amount": "1362891"
+ },
+ "monthly_cheer_amount": {
+ "amount": 0
+ },
+ "weekly_cheer_amount": {
+ "amount": 0
+ },
+ "30day_cheer_amount": {
+ "amount": 10
+ },
+ "session_cheer_amount": {
+ "amount": 10
+ },
+ "all_time_top_cheerer": {
+ "name": "RLC1389",
+ "amount": 267749
+ },
+ "monthly_top_cheerer": "",
+ "weekly_top_cheerer": "",
+ "30day_top_cheerer": {
+ "name": "joXuahardylee",
+ "amount": 10
+ },
+ "session_top_cheerer": {
+ "name": "joXuahardylee",
+ "amount": 10
+ },
+ "all_time_top_cheerers": [
+ {
+ "name": "RLC1389",
+ "amount": 267749
+ },
+ {
+ "name": "VikingDude78",
+ "amount": 189466
+ },
+ {
+ "name": "lostmyshirts",
+ "amount": 182836
+ },
+ {
+ "name": "AmaranthineMemories",
+ "amount": 119550
+ },
+ {
+ "name": "SaBiN666999",
+ "amount": 55913
+ },
+ {
+ "name": "QuietStorm90",
+ "amount": 46000
+ },
+ {
+ "name": "Radio_Hatice",
+ "amount": 38400
+ },
+ {
+ "name": "chainsofthought",
+ "amount": 37667
+ },
+ {
+ "name": "swedish1975",
+ "amount": 31300
+ },
+ {
+ "name": "HitmanptZ",
+ "amount": 30045
+ }
+ ],
+ "monthly_top_cheerers": "",
+ "weekly_top_cheerers": "",
+ "30day_top_cheerers": [
+ {
+ "name": "joXuahardylee",
+ "amount": 10
+ }
+ ],
+ "session_top_cheerers": [
+ {
+ "name": "joXuahardylee",
+ "amount": 10
+ }
+ ],
+ "all_time_top_cheers": [
+ {
+ "name": "Radio_Hatice",
+ "amount": 20000,
+ "message": "Party10000 Party10000"
+ },
+ {
+ "name": "jdbandshirts",
+ "amount": 15000,
+ "message": "Cheer10000 Cheer5000"
+ },
+ {
+ "name": "AmaranthineMemories",
+ "amount": 12000,
+ "message": "cheer12000 absolutely love that song"
+ },
+ {
+ "name": "jdbandshirts",
+ "amount": 10000,
+ "message": "Cheer10000"
+ },
+ {
+ "name": "AmaranthineMemories",
+ "amount": 10000,
+ "message": "Cheer10000 dropping back by to give you ur gift V. Helping my dad out. Never stop being who you are. Happy birthday again."
+ },
+ {
+ "name": "vikingdude78",
+ "amount": 10000,
+ "message": "Cheer10000"
+ },
+ {
+ "name": "vikingdude78",
+ "amount": 10000,
+ "message": "Cheer10000"
+ },
+ {
+ "name": "RLC1389",
+ "amount": 10000,
+ "message": "Cheer10000"
+ },
+ {
+ "name": "AmaranthineMemories",
+ "amount": 10000,
+ "message": "cheer10000 never stop being you V"
+ },
+ {
+ "name": "BadwolfTam",
+ "amount": 10000,
+ "message": "Cheer10000 90% is better than most..."
+ }
+ ],
+ "monthly_top_cheers": [],
+ "30day_top_cheers": [
+ {
+ "name": "joXuahardylee",
+ "amount": 10,
+ "message": "ShowLove10 how are you Vicky? ??? I'm Happy to see you again"
+ }
+ ],
+ "weekly_top_cheers": [],
+ "session_top_cheers": [
+ {
+ "name": "joXuahardylee",
+ "amount": 10,
+ "message": "ShowLove10 how are you Vicky? ??? I'm Happy to see you again"
+ }
+ ],
+ "all_time_top_sub_gifters": [
+ {
+ "name": "RLC1389",
+ "count": 2155
+ },
+ {
+ "name": "SaBiN666999",
+ "count": 711
+ },
+ {
+ "name": "pedrodyffryn",
+ "count": 659
+ },
+ {
+ "name": "vikingdude78",
+ "count": 618
+ },
+ {
+ "name": "tyreseabraham19",
+ "count": 598
+ },
+ {
+ "name": "Gaveup_",
+ "count": 484
+ },
+ {
+ "name": "chainsofthought",
+ "count": 363
+ },
+ {
+ "name": "AmaranthineMemories",
+ "count": 333
+ },
+ {
+ "name": "PedroCavaleiro",
+ "count": 278
+ },
+ {
+ "name": "troyx66",
+ "count": 250
+ }
+ ],
+ "monthly_top_sub_gifters": [
+ {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ }
+ ],
+ "weekly_top_sub_gifters": [],
+ "30day_top_sub_gifters": [
+ {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ }
+ ],
+ "session_top_sub_gifters": [
+ {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ }
+ ],
+ "most_recent_sub_gifter": {
+ "name": "lthe_muffin_manl"
+ },
+ "session_sub_gifters": [
+ {
+ "name": "lthe_muffin_manl"
+ }
+ ],
+ "session_most_recent_sub_gifter": {
+ "name": "lthe_muffin_manl"
+ },
+ "all_time_top_sub_gifter": {
+ "name": "RLC1389",
+ "count": 2155
+ },
+ "monthly_top_sub_gifter": {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ },
+ "weekly_top_sub_gifter": "",
+ "30day_top_sub_gifter": {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ },
+ "session_top_sub_gifter": {
+ "name": "lthe_muffin_manl",
+ "count": 1
+ },
+ "monthly_top_subscriber": {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ "weekly_top_subscriber": {
+ "name": "austros99",
+ "months": 1
+ },
+ "30day_top_subscriber": {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ "session_top_subscriber": {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ "monthly_top_subscribers": [
+ {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ {
+ "name": "niksthekicks",
+ "months": 61
+ },
+ {
+ "name": "Tim_Volkihar",
+ "months": 55
+ },
+ {
+ "name": "tonimackerz",
+ "months": 51
+ },
+ {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ {
+ "name": "metaltvtwitch",
+ "months": 48
+ },
+ {
+ "name": "DefinitelyNotBR",
+ "months": 34
+ },
+ {
+ "name": "mis0_13",
+ "months": 23
+ },
+ {
+ "name": "tokyoraven83",
+ "months": 1
+ },
+ {
+ "name": "LostBandit",
+ "months": 1
+ }
+ ],
+ "weekly_top_subscribers": [
+ {
+ "name": "austros99",
+ "months": 1
+ }
+ ],
+ "30day_top_subscribers": [
+ {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ {
+ "name": "niksthekicks",
+ "months": 61
+ },
+ {
+ "name": "HitmanptZ",
+ "months": 60
+ },
+ {
+ "name": "GiannisSatanas",
+ "months": 57
+ },
+ {
+ "name": "Radio_Hatice",
+ "months": 55
+ },
+ {
+ "name": "Tim_Volkihar",
+ "months": 55
+ },
+ {
+ "name": "tonimackerz",
+ "months": 51
+ },
+ {
+ "name": "tonimackerz",
+ "months": 50
+ },
+ {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ {
+ "name": "metaltvtwitch",
+ "months": 48
+ }
+ ],
+ "session_top_subscribers": [
+ {
+ "name": "QuietStorm90",
+ "months": 61
+ },
+ {
+ "name": "niksthekicks",
+ "months": 61
+ },
+ {
+ "name": "HitmanptZ",
+ "months": 60
+ },
+ {
+ "name": "GiannisSatanas",
+ "months": 57
+ },
+ {
+ "name": "Radio_Hatice",
+ "months": 55
+ },
+ {
+ "name": "Tim_Volkihar",
+ "months": 55
+ },
+ {
+ "name": "tonimackerz",
+ "months": 51
+ },
+ {
+ "name": "tonimackerz",
+ "months": 50
+ },
+ {
+ "name": "StateOfRageNGrace",
+ "months": 50
+ },
+ {
+ "name": "metaltvtwitch",
+ "months": 48
+ }
+ ],
+ "_id": "54be3c8e505083ee78fae68ee0fed97c",
+ "priority": 10
+ }
+ },
+ "event_id": "evt_0846e73d9f54a465731b74bdb85ddf48"
+ }
+]
diff --git a/test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs b/test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs
index 8d3217d..ba3b0ac 100644
--- a/test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs
+++ b/test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs
@@ -43,6 +43,7 @@ public static IEnumerable> GetData()
yield return () => new JsonFile("streamlabels.json", typeof(StreamlabelsEvent));
yield return () => new JsonFile("streamlabelsUnderlying.json", typeof(StreamlabelsUnderlyingEvent));
yield return () => new JsonFile("streamlabelsUnderlying2.json", typeof(StreamlabelsUnderlyingEvent));
+ yield return () => new JsonFile("streamlabelsUnderlying3.json", typeof(StreamlabelsUnderlyingEvent));
yield return () => new JsonFile("subMysteryGift.json", typeof(SubMysteryGiftEvent));
yield return () => new JsonFile("subMysteryGift1.json", typeof(SubMysteryGiftEvent));
yield return () => new JsonFile("subscription.json", typeof(SubscriptionEvent));
From 936fe7e1799bc0f6f9c483456925f3fb93427090 Mon Sep 17 00:00:00 2001
From: Samuel Meenzen
Date: Sun, 22 Feb 2026 14:52:13 +0100
Subject: [PATCH 5/7] chore: cleanup
---
...labelsUnderlyingMessageDonationGoal.cs => DonationGoal.cs} | 2 +-
.../Messages/DataTypes/StreamlabelsMessageData.cs | 2 +-
.../Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
rename src/Streamlabs.SocketClient/Messages/DataTypes/{StreamlabelsUnderlyingMessageDonationGoal.cs => DonationGoal.cs} (84%)
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/DonationGoal.cs
similarity index 84%
rename from src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
rename to src/Streamlabs.SocketClient/Messages/DataTypes/DonationGoal.cs
index 48ae8ae..5cbc0bb 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageDonationGoal.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/DonationGoal.cs
@@ -2,7 +2,7 @@
namespace Streamlabs.SocketClient.Messages.DataTypes;
-public sealed record StreamlabelsUnderlyingMessageDonationGoal {
+public sealed record DonationGoal {
[JsonPropertyName("title")]
public required string Title { get; init; }
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
index afa3663..afa783a 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
@@ -138,7 +138,7 @@ public sealed record StreamlabelsMessageData : IHasMessageId, IHasPriority
public required string SessionMostRecentMonthlyDonator { get; init; }
[JsonPropertyName("cloudbot_counter_deaths")]
- public string? CloudbotCounterDeaths { get; init; } // this property might not be present
+ public string? CloudbotCounterDeaths { get; init; }
[JsonPropertyName("monthly_subscriber_count")]
[JsonConverter(typeof(IntStringConverter))]
diff --git a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
index 508c4a3..672ab83 100644
--- a/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
+++ b/src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
@@ -7,8 +7,8 @@ namespace Streamlabs.SocketClient.Messages.DataTypes;
public sealed record StreamlabelsUnderlyingMessageData : IHasMessageId, IHasPriority
{
[JsonPropertyName("donation_goal")]
- [JsonConverter(typeof(FlexibleObjectConverter))]
- public StreamlabelsUnderlyingMessageDonationGoal? DonationGoal { get; init; }
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public DonationGoal? DonationGoal { get; init; }
[JsonPropertyName("most_recent_donator")]
public required Donator MostRecentDonator { get; init; }
From 1cad1f6f147d8c1c244091a86f439b07261b1217 Mon Sep 17 00:00:00 2001
From: Samuel Meenzen
Date: Sun, 22 Feb 2026 15:15:34 +0100
Subject: [PATCH 6/7] chore: add tests
---
.../FlexibleObjectConverterTests.cs | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs
diff --git a/test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs b/test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs
new file mode 100644
index 0000000..1cd7a1a
--- /dev/null
+++ b/test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs
@@ -0,0 +1,100 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Streamlabs.SocketClient.Converters;
+
+namespace Streamlabs.SocketClient.Tests.Converters;
+
+[SuppressMessage("Style", "VSTHRD200:Use \"Async\" suffix for async methods")]
+public class FlexibleObjectConverterTests
+{
+ private sealed class SampleClass
+ {
+ [JsonConverter(typeof(FlexibleObjectConverter))]
+ public SamplePayload? Payload { get; set; }
+ }
+
+ private sealed class SamplePayload
+ {
+ public string? Name { get; set; }
+ public int Count { get; set; }
+ }
+
+ [Test]
+ public async Task Read_ObjectToken_Deserializes()
+ {
+ // Arrange
+ var json = """{"Payload":{"Name":"Alpha","Count":3}}""";
+
+ // Act
+ var result = JsonSerializer.Deserialize(json);
+
+ // Assert
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result!.Payload).IsNotNull();
+ await Assert.That(result.Payload!.Name).IsEqualTo("Alpha");
+ await Assert.That(result.Payload.Count).IsEqualTo(3);
+ }
+
+ [Test]
+ public async Task Read_EscapedJsonString_Deserializes()
+ {
+ // Arrange
+ var json = """{"Payload":"{\"Name\":\"Beta\",\"Count\":7}"}""";
+
+ // Act
+ var result = JsonSerializer.Deserialize(json);
+
+ // Assert
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result!.Payload).IsNotNull();
+ await Assert.That(result.Payload!.Name).IsEqualTo("Beta");
+ await Assert.That(result.Payload.Count).IsEqualTo(7);
+ }
+
+ [Test]
+ public async Task Read_PlainString_ReturnsNull()
+ {
+ // Arrange
+ var json = """{"Payload":"just a plain string"}""";
+
+ // Act
+ var result = JsonSerializer.Deserialize(json);
+
+ // Assert
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result!.Payload).IsNull();
+ }
+
+ [Test]
+ public async Task Read_MalformedJsonString_ReturnsNull()
+ {
+ // Arrange
+ var json = """{"Payload":"{not valid json}"}""";
+
+ // Act
+ var result = JsonSerializer.Deserialize(json);
+
+ // Assert
+ await Assert.That(result).IsNotNull();
+ await Assert.That(result!.Payload).IsNull();
+ }
+
+ [Test]
+ public async Task Write_SerializesNormally()
+ {
+ // Arrange
+ SampleClass sample = new()
+ {
+ Payload = new SamplePayload { Name = "Gamma", Count = 42 },
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(sample);
+
+ // Assert
+ await Assert.That(json).Contains("\"Payload\"");
+ await Assert.That(json).Contains("\"Name\":\"Gamma\"");
+ await Assert.That(json).Contains("\"Count\":42");
+ }
+}
From 2eb478ccdf10cab8828a6b473ce9b22c9a58b096 Mon Sep 17 00:00:00 2001
From: Samuel Meenzen
Date: Sun, 22 Feb 2026 15:16:15 +0100
Subject: [PATCH 7/7] chore: cleanup
---
.../Converters/FlexibleObjectConverter.cs | 63 +++++++++----------
.../SerializationExtensions.cs | 14 ++++-
2 files changed, 41 insertions(+), 36 deletions(-)
diff --git a/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs b/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
index 33acd44..606c399 100644
--- a/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
+++ b/src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using Streamlabs.SocketClient.InternalExtensions;
namespace Streamlabs.SocketClient.Converters;
@@ -12,46 +13,38 @@ namespace Streamlabs.SocketClient.Converters;
public class FlexibleObjectConverter : JsonConverter
where T : class
{
- public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- switch (reader.TokenType)
+ public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
+ reader.TokenType switch
{
- case JsonTokenType.StartObject or JsonTokenType.StartArray:
- return JsonSerializer.Deserialize(ref reader, options);
- case JsonTokenType.String:
- {
- string? value = reader.GetString();
- if (value == null)
- {
- return null;
- }
-
- string trimmed = value.Trim();
+ JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, options),
+ JsonTokenType.StartArray => JsonSerializer.Deserialize(ref reader, options),
+ JsonTokenType.String => DeserializeString(ref reader, options),
+ _ => null,
+ };
- bool isJsonObject = trimmed.Length > 0 && trimmed[0] == '{' && trimmed[^1] == '}';
- bool isJsonArray = trimmed.Length > 0 && trimmed[0] == '[' && trimmed[^1] == ']';
-
- if (isJsonObject || isJsonArray)
- {
- try
- {
- return JsonSerializer.Deserialize(trimmed, options);
- }
- catch (JsonException)
- {
- return null;
- }
- }
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
+ JsonSerializer.Serialize(writer, value, options);
- break;
- }
+ private static T? DeserializeString(ref Utf8JsonReader reader, JsonSerializerOptions options)
+ {
+ string? value = reader.GetString();
+ if (value is null)
+ {
+ return null;
}
- return null;
- }
+ if (!value.IsJsonObjectOrArray())
+ {
+ return null;
+ }
- public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
- {
- JsonSerializer.Serialize(writer, value, options);
+ try
+ {
+ return JsonSerializer.Deserialize(value.Trim(), options);
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
}
}
diff --git a/src/Streamlabs.SocketClient/InternalExtensions/SerializationExtensions.cs b/src/Streamlabs.SocketClient/InternalExtensions/SerializationExtensions.cs
index e98c8b4..ba5c13d 100644
--- a/src/Streamlabs.SocketClient/InternalExtensions/SerializationExtensions.cs
+++ b/src/Streamlabs.SocketClient/InternalExtensions/SerializationExtensions.cs
@@ -13,11 +13,23 @@ internal static class SerializationExtensions
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
- private static readonly IReadOnlyCollection Empty = Array.Empty();
+ private static readonly IReadOnlyCollection Empty = [];
public static IReadOnlyCollection Deserialize(this string json)
{
string normalized = json.NormalizeTypeDiscriminators();
return JsonSerializer.Deserialize>(normalized, Options) ?? Empty;
}
+
+ public static bool IsJsonObjectOrArray(this string value)
+ {
+ string trimmed = value.Trim();
+ return trimmed switch
+ {
+ "" => false,
+ ['{', .., '}'] => true,
+ ['[', .., ']'] => true,
+ _ => false,
+ };
+ }
}