Skip to content

Map invisible to null when the wire omits it - #6673

Merged
gpunto merged 2 commits into
developfrom
migrate/invisible-null
Sep 1, 2026
Merged

gpunto merged 2 commits into
developfrom
migrate/invisible-null

Conversation

@gpunto

@gpunto gpunto commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Goal

invisible is only ever sent for the connected user. The backend marks it ignore_if_client_side on the shared user payload and declares it unconditionally only on the own-user response, so for every other user the field never reaches a client-side SDK.

The hand-written DTOs defaulted it to false regardless, asserting something the server did not send. Endpoints already migrated to the generated UserResponse return null, because the generated model has no such field. The two parse paths disagreed on the same payload, and the disagreement is observable: the same message parsed over HTTP and over the websocket produced users differing on this field.

Closes AND-1458

Implementation

  • Default DownstreamUserDto.invisible and the direct UserAdapter to null rather than false, so all parse paths agree with the wire.
  • Move the expectations whose fixture JSON does not declare invisible to null. Fixtures that do send it are unchanged: UserTestData.expectedAllFields (true), expectedWithExplicitNulls (null), and the event fixtures, whose JSON carries the field.

Consumer impact

  • User.invisible returns null instead of false for any user other than the connected one. The declared type is unchanged (Boolean?) and the API diff is empty.
  • User.isInvisible is invisible == true, so it still returns false and code using the accessor is unaffected.
  • The connected user is unaffected, since the connect payload always carries the field.
  • Offline storage flattens the value: UserEntity.invisible is non-null and written from isInvisible, so a user read back from the database reports false.
  • Client-side sorting on invisible orders null before false, as getComparableField exposes the raw nullable.

Testing

  • Device probe against a live app: the connected user keeps a non-null invisible from the connect payload, while members, reads, message senders, poll voters and poll creators across three channels all report null.
  • Reverting either default to false fails 22 tests, so the value is pinned on both the DTO and the direct parse path.
  • UserTestData covers the three wire cases explicitly: field sent as true, sent as null, and absent.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected user visibility data handling when the invisible field is omitted.
    • Omitted values now remain null instead of being incorrectly interpreted as false.
    • Preserved explicit visibility values when provided by the server.
  • Tests

    • Updated message, reaction, poll, event, and user parsing scenarios to reflect the corrected nullable behavior.

@gpunto gpunto added the pr:improvement Improvement label Aug 31, 2026
@github-actions

github-actions Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@github-actions

github-actions Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 6.11 MB 6.11 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.42 MB 11.42 MB 0.00 MB 🟢
stream-chat-android-compose 12.90 MB 12.90 MB 0.00 MB 🟢

@gpunto
gpunto marked this pull request as ready for review August 31, 2026 11:44
@gpunto
gpunto requested a review from a team as a code owner August 31, 2026 11:44
@gpunto
gpunto marked this pull request as draft August 31, 2026 11:45
@gpunto
gpunto marked this pull request as ready for review August 31, 2026 11:45
@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ea4b543-fed6-48af-8f6d-92198f3de9ad

📥 Commits

Reviewing files that changed from the base of the PR and between 0a3a5c2 and 476ae26.

📒 Files selected for processing (7)
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/UserDtos.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserAdapter.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/MessageTestData.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/NewMessageEventTestData.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/PollTestData.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/ReactionTestData.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/UserTestData.kt

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


Walkthrough

The parser now preserves null for an absent invisible user field. Message, event, poll, reaction, and user fixtures now expect this value.

Changes

User invisible nullability

Layer / File(s) Summary
Nullable parser default
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/UserDtos.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/direct/UserAdapter.kt
User DTO construction and direct JSON parsing now default an absent invisible field to null.
Parser fixture expectations
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/testdata/*
Expected users in message, event, poll, reaction, and user fixtures now use invisible = null when the field is omitted.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 476ae

Users other than the connected user will now retain an unknown visibility value when the server omits the field, while explicit visibility and the boolean convenience behavior remain unchanged; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: velikovpetar

Poem

A rabbit checks the fields at night

Missing shadows stay null and light
The fixtures hop in matching rows
No false appears where absence shows
The parser rests; the contract grows

🚥 Pre-merge checks | ✅ 4 | ❌ 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%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: mapping omitted wire values for invisible to null.
Description check ✅ Passed The description covers the goal, implementation, consumer impact, and testing. It explains the API behavior, affected parse paths, storage behavior, and test coverage. The UI, checklist, and GIF templ…
Full details: Description check

Explanation

The description covers the goal, implementation, consumer impact, and testing. It explains the API behavior, affected parse paths, storage behavior, and test coverage. The UI, checklist, and GIF template sections are omitted, but they are not critical for this non-UI change.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate/invisible-null

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.

@andremion andremion left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. Two nits inline, plus one scope question.

The DB entity and the outgoing user requests still flatten this to false. Any plan to follow up on those, or not worth it while nothing in the SDK reads the field?

@gpunto

gpunto commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

On the scope question: neither is changed by this PR, but they are worth separate follow-ups for different reasons.

Outgoing requests are unaffected here, because both UpstreamUserDto and UserRequest are built from isInvisible, which is false for null and for false alike. There is a latent issue though: the generated UserRequest.invisible is nullable, so it could carry "unspecified" through as an omitted field, while today we always assert a concrete false. That means connecting with a User that never set the field sends invisible: false rather than leaving it alone. Changing it would change what we send, so it does not belong in a parse-path PR.

The DB entity is the one with a visible consequence. UserEntity.invisible is a non-null Boolean written from isInvisible, so null flattens to false on the way in and reads back as false. Before this PR network and cache agreed on false; after it, the same user reads null fresh from the network and false once cached. isInvisible still hides the difference, but anyone comparing user.invisible == false gets an answer that depends on where the object came from. Making the column nullable fixes it and keeps own-user fidelity, since isInvisible is lossless for a user whose value is always set. The cost is a Room schema bump, and the DB uses fallbackToDestructiveMigration, so it drops the offline cache. Worth batching with the next users-table change rather than spending a wipe on a field whose only in-SDK reader is isInvisible.

Happy to file both, or to pull the DB one into this PR if you would rather not leave the asymmetry in place at all.

@sonarqubecloud

Copy link
Copy Markdown

@andremion

Copy link
Copy Markdown
Contributor

Both as follow-ups sounds right. The destructive migration decides it for me: batching the nullable column with the next users-table change is better than spending a cache wipe on a field whose only reader in the SDK is isInvisible. No need to pull it into this PR.

Same for the request side, changing what we send does not belong in a parse-path PR.

@gpunto
gpunto added this pull request to the merge queue Sep 1, 2026
Merged via the queue into develop with commit e77c798 Sep 1, 2026
19 checks passed
@gpunto
gpunto deleted the migrate/invisible-null branch September 1, 2026 11:51
@stream-public-bot stream-public-bot added the released Included in a release label Sep 1, 2026
@stream-public-bot

Copy link
Copy Markdown
Contributor

🚀 Available in v7.10.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Improvement released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants