Skip to content

fix: handle more streamlabels event variations#289

Merged
meenzen merged 8 commits intomainfrom
PedroCavaleiro/main
Feb 22, 2026
Merged

fix: handle more streamlabels event variations#289
meenzen merged 8 commits intomainfrom
PedroCavaleiro/main

Conversation

@meenzen
Copy link
Owner

@meenzen meenzen commented Feb 22, 2026

supersedes #283

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved robustness of stream labels data handling, including better support for donation goals, top donors, and related metrics from API responses.

PedroCavaleiro and others added 8 commits February 17, 2026 14:51
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.
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces a flexible JSON deserialization mechanism for handling stream-labels data. It adds a generic FlexibleObjectConverter<T> supporting both standard JSON objects/arrays and string-encoded JSON, defines a new DonationGoal record type, and refactors StreamlabelsUnderlyingMessageData properties from required strings to nullable types with converter support.

Changes

Cohort / File(s) Summary
JSON Converter Infrastructure
src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs, src/Streamlabs.SocketClient/InternalExtensions/SerializationExtensions.cs
New generic converter FlexibleObjectConverter<T> handles deserialization from JSON objects, arrays, or escaped string payloads with validation via IsJsonObjectOrArray() extension method. Added extension validates whether a trimmed string represents valid JSON object or array syntax.
Data Type Definitions
src/Streamlabs.SocketClient/Messages/DataTypes/DonationGoal.cs, src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsMessageData.cs
Introduces new DonationGoal sealed record with Title, CurrentAmount, and GoalAmount properties. Modifies StreamlabelsMessageData to make CloudbotCounterDeaths nullable.
Refactored Message Data Structure
src/Streamlabs.SocketClient/Messages/DataTypes/StreamlabelsUnderlyingMessageData.cs
Converts 20+ properties from required strings/collections to nullable types with FlexibleObjectConverter attributes. Changes affect donation goals, donators, top donators, monthly donators, counters, and related collection fields to support flexible deserialization patterns.
Test Coverage
test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs, test/Streamlabs.SocketClient.Tests/MessageTypeTests.cs, test/Streamlabs.SocketClient.Tests/MessageJson/streamlabelsUnderlying3.json
New test suite validates converter behavior across standard objects, escaped JSON strings, malformed payloads, and serialization roundtrips. Adds comprehensive test fixture with full stream-labels underlying data payload and corresponding test case entry.

Sequence Diagram(s)

sequenceDiagram
    participant JsonReader as JSON Reader
    participant Converter as FlexibleObjectConverter
    participant Validator as IsJsonObjectOrArray()
    participant Deserializer as JsonSerializer
    participant Target as T (Target Type)

    JsonReader->>Converter: Read(token)
    alt StartObject or StartArray
        Converter->>Deserializer: Deserialize directly
        Deserializer->>Target: Create instance
    else String Token
        Converter->>Validator: Validate string content
        alt Valid JSON structure
            Validator-->>Converter: true
            Converter->>Deserializer: Deserialize trimmed string
            Deserializer->>Target: Create instance
        else Invalid structure
            Validator-->>Converter: false
            Converter->>Target: Return null
        end
    else Other Token
        Converter->>Target: Return null
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 A converter hops in to make JSON quite flexible,
Handling objects and strings with grace so respectable,
DonationGoals now bloom, properties take new shape,
From required to nullable, the refactor helps escape,
Tests verify the path where data transforms and flows! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: handle more streamlabels event variations' directly addresses the main objective: improving deserialization of StreamLabels events by introducing flexible JSON handling via FlexibleObjectConverter and making various properties nullable.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch PedroCavaleiro/main

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

@github-actions
Copy link

Test Results

53 tests  +7   53 ✅ +7   1s ⏱️ ±0s
 1 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit 2eb478c. ± Comparison against base commit 560705f.

@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 86.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.17%. Comparing base (bdb9e46) to head (2eb478c).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
...SocketClient/Converters/FlexibleObjectConverter.cs 77.27% 3 Missing and 2 partials ⚠️
...ient/InternalExtensions/SerializationExtensions.cs 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #289      +/-   ##
==========================================
- Coverage   80.18%   80.17%   -0.01%     
==========================================
  Files          59       61       +2     
  Lines         777      812      +35     
  Branches       54       58       +4     
==========================================
+ Hits          623      651      +28     
- Misses        125      129       +4     
- Partials       29       32       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs (2)

13-14: Consider sealing the class.

Nothing prevents subclassing FlexibleObjectConverter<T>. Unless extensibility is intentional, adding sealed removes that surface area.

♻️ Proposed change
-public class FlexibleObjectConverter<T> : JsonConverter<T>
+public sealed class FlexibleObjectConverter<T> : JsonConverter<T>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs` around
lines 13 - 14, The FlexibleObjectConverter<T> class is currently inheritable; if
extensibility is not required, mark the class as sealed by adding the sealed
modifier to its declaration (public sealed class FlexibleObjectConverter<T> :
JsonConverter<T>) to prevent subclassing; keep it non-sealed only if you
intentionally expect consumers to derive from FlexibleObjectConverter<T>.

25-26: Write will infinitely recurse if this converter is ever applied at the class level of T.

JsonSerializer.Serialize(writer, value, options) causes the serializer to look up a converter for the runtime type of value. If FlexibleObjectConverter<T> is ever placed on the class declaration of T (instead of only on properties), this would call WriteSerializeWrite → … indefinitely. Since the converter is currently only used via property-level [JsonConverter] attributes it is safe today, but consider documenting this constraint or guarding against it.

📝 Suggested doc comment
 /// <summary>
 /// Provides a flexible JSON converter for <typeparamref name="T"/> ...
+/// <para>
+/// This converter must only be applied at the <em>property</em> level via <c>[JsonConverter(...)]</c>.
+/// Applying it as a class-level attribute on <typeparamref name="T"/> itself will cause infinite
+/// recursion during serialization.
+/// </para>
 /// </summary>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs` around
lines 25 - 26, The Write method on FlexibleObjectConverter<T> will recurse if
the converter is applied to type T because JsonSerializer.Serialize(writer,
value, options) will resolve this converter again; fix by using the overload
that supplies the runtime type to JsonSerializer.Serialize (so the serializer
uses the concrete runtime type instead of looking up the converter for T) when
writing (use value.GetType() or fallback to typeof(T) for null), and add a brief
XML doc comment on class FlexibleObjectConverter<T> noting it must not be
applied at the type level (or that the runtime-type overload is used to prevent
recursion).
test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs (1)

23-99: Missing test for StartArray token.

The converter's StartArray branch is used in production for every IReadOnlyCollection<T>? property but has no coverage here. Add a test with an array-typed SamplePayload to verify that path deserializes correctly.

✅ Suggested test
private sealed class SampleListClass
{
    [JsonConverter(typeof(FlexibleObjectConverter<IReadOnlyCollection<SamplePayload>>))]
    public IReadOnlyCollection<SamplePayload>? Items { get; set; }
}

[Test]
public async Task Read_ArrayToken_DeserializesList()
{
    // Arrange
    var json = """{"Items":[{"Name":"Alpha","Count":1},{"Name":"Beta","Count":2}]}""";

    // Act
    var result = JsonSerializer.Deserialize<SampleListClass>(json);

    // Assert
    await Assert.That(result).IsNotNull();
    await Assert.That(result!.Items).IsNotNull();
    await Assert.That(result.Items!.Count).IsEqualTo(2);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs`
around lines 23 - 99, Add a unit test to cover the converter's StartArray branch
by creating a SampleListClass with an Items property annotated with
[JsonConverter(typeof(FlexibleObjectConverter<IReadOnlyCollection<SamplePayload>>))],
then add a test method Read_ArrayToken_DeserializesList that deserializes json
like {"Items":[{"Name":"Alpha","Count":1},{"Name":"Beta","Count":2}]} into
SampleListClass and asserts the result and Items are not null and Items.Count ==
2; use the same assertion style as existing tests and the
JsonSerializer.Deserialize<SampleListClass> call to exercise
FlexibleObjectConverter's array handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs`:
- Around line 16-23: The Read method in FlexibleObjectConverter<T> handles
JsonException differently between branches: DeserializeString swallows
JsonException and returns null, but the StartObject/StartArray branches call
JsonSerializer.Deserialize<T>(ref reader, options) and let exceptions propagate;
make them consistent by wrapping the StartObject and StartArray deserialize
calls in a try/catch(JsonException) that returns null on error (same behavior as
DeserializeString) so malformed/structurally-mismatched payloads for
FlexibleObjectConverter<TopDonator> don’t crash message deserialization.

---

Nitpick comments:
In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs`:
- Around line 13-14: The FlexibleObjectConverter<T> class is currently
inheritable; if extensibility is not required, mark the class as sealed by
adding the sealed modifier to its declaration (public sealed class
FlexibleObjectConverter<T> : JsonConverter<T>) to prevent subclassing; keep it
non-sealed only if you intentionally expect consumers to derive from
FlexibleObjectConverter<T>.
- Around line 25-26: The Write method on FlexibleObjectConverter<T> will recurse
if the converter is applied to type T because JsonSerializer.Serialize(writer,
value, options) will resolve this converter again; fix by using the overload
that supplies the runtime type to JsonSerializer.Serialize (so the serializer
uses the concrete runtime type instead of looking up the converter for T) when
writing (use value.GetType() or fallback to typeof(T) for null), and add a brief
XML doc comment on class FlexibleObjectConverter<T> noting it must not be
applied at the type level (or that the runtime-type overload is used to prevent
recursion).

In
`@test/Streamlabs.SocketClient.Tests/Converters/FlexibleObjectConverterTests.cs`:
- Around line 23-99: Add a unit test to cover the converter's StartArray branch
by creating a SampleListClass with an Items property annotated with
[JsonConverter(typeof(FlexibleObjectConverter<IReadOnlyCollection<SamplePayload>>))],
then add a test method Read_ArrayToken_DeserializesList that deserializes json
like {"Items":[{"Name":"Alpha","Count":1},{"Name":"Beta","Count":2}]} into
SampleListClass and asserts the result and Items are not null and Items.Count ==
2; use the same assertion style as existing tests and the
JsonSerializer.Deserialize<SampleListClass> call to exercise
FlexibleObjectConverter's array handling.

Comment on lines +16 to +23
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch
{
JsonTokenType.StartObject => JsonSerializer.Deserialize<T>(ref reader, options),
JsonTokenType.StartArray => JsonSerializer.Deserialize<T>(ref reader, options),
JsonTokenType.String => DeserializeString(ref reader, options),
_ => null,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent exception handling between StartObject/StartArray and DeserializeString.

DeserializeString silently returns null on any JsonException, but the StartObject and StartArray branches propagate exceptions to the caller. Two realistic failure modes:

  1. The API sends an array for a property typed as FlexibleObjectConverter<TopDonator> (not a collection). JsonSerializer.Deserialize<TopDonator>(ref reader, options) on an array token would throw a JsonException that crashes the whole message deserialization.
  2. The API sends a structurally valid object/array token but with missing required fields on the target type.

For the same resilience as DeserializeString, wrap the object/array branches consistently:

🛡️ Proposed fix — consistent exception handling
 public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
     reader.TokenType switch
     {
-        JsonTokenType.StartObject => JsonSerializer.Deserialize<T>(ref reader, options),
-        JsonTokenType.StartArray => JsonSerializer.Deserialize<T>(ref reader, options),
+        JsonTokenType.StartObject => TryDeserializeToken(ref reader, options),
+        JsonTokenType.StartArray => TryDeserializeToken(ref reader, options),
         JsonTokenType.String => DeserializeString(ref reader, options),
         _ => null,
     };

+private static T? TryDeserializeToken(ref Utf8JsonReader reader, JsonSerializerOptions options)
+{
+    try
+    {
+        return JsonSerializer.Deserialize<T>(ref reader, options);
+    }
+    catch (JsonException)
+    {
+        return null;
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Streamlabs.SocketClient/Converters/FlexibleObjectConverter.cs` around
lines 16 - 23, The Read method in FlexibleObjectConverter<T> handles
JsonException differently between branches: DeserializeString swallows
JsonException and returns null, but the StartObject/StartArray branches call
JsonSerializer.Deserialize<T>(ref reader, options) and let exceptions propagate;
make them consistent by wrapping the StartObject and StartArray deserialize
calls in a try/catch(JsonException) that returns null on error (same behavior as
DeserializeString) so malformed/structurally-mismatched payloads for
FlexibleObjectConverter<TopDonator> don’t crash message deserialization.

@meenzen meenzen merged commit 6e28aab into main Feb 22, 2026
11 checks passed
@meenzen meenzen deleted the PedroCavaleiro/main branch February 22, 2026 14:50
@coderabbitai coderabbitai bot mentioned this pull request Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants