Skip to content

[Breaking] Split generated model Codable into Encodable and Decodable - #1289

Draft
martinmitrevski wants to merge 1 commit into
reduce-sdk-size-api-clientfrom
reduce-sdk-size-codable-split
Draft

martinmitrevski wants to merge 1 commit into
reduce-sdk-size-api-clientfrom
reduce-sdk-size-codable-split

Conversation

@martinmitrevski

Copy link
Copy Markdown
Contributor

Stacked on #1286 — base is reduce-sdk-size-api-client, so the diff here is only the conformance work. The SDK Size job compares against the stored develop benchmark, so the number it reports includes #1286 underneath.

🚨 This is a source-breaking change and needs a major. Opening it to get CI size numbers and a read on the policy, not to merge as-is.

🔗 Issue Links

Follows #1286.

🎯 Goal

Sources/StreamVideo/OpenApi/generated is the largest SDK-owned area of the binary — 2,986 KB of 10,355 KB (33%). Most of it is synthesized from public + Codable + Hashable on the models: init(from:) 278 KB, encode(to:) 196 KB, == 162 KB, hash(into:) 147 KB, witness tables 250 KB.

A request model is only ever encoded and a response model is only ever decoded, so roughly half of that Codable machinery is synthesized for calls nobody makes.

📝 Summary

  • Request models conform to Encodable, response models to Decodable, instead of both conforming to Codable.
  • The split is derived from the generated client and locked to Scripts/generated-model-conformance.json — 249 entries, one per model.
  • ALWAYS_CODABLE in the codegen script pins models that keep both halves.
  • StreamVideo: 10,355.1 KB → 10,012.4 KB (−342.6 KB, −3.31%) combined with [Improvement] Reduce SDK size in the generated API client #1286; −262.7 KB for this PR alone. Release/arm64/device, stripped.

🛠 Implementation

Deriving the split. Response types come from jsonDecoder.decode(X.self) in DefaultAPI plus the WebSocket decode sites; request types from the request: parameter types. Each set is closed over stored-property types, because a Decodable class needs every property type to be Decodable too. Result: 111 Decodable, 64 Encodable, 74 staying Codable.

JSONEncodable cannot outlive Encodable. StreamCore supplies encodeToJSON() only via extension JSONEncodable where Self: Encodable, so a response model that drops Encodable drops JSONEncodable with it — two public conformances per type, not one.

Locking. A conformance is public API, so the derived result is checked in and compared on every run. Drift fails the script before it touches a file:

error: the derived model conformances no longer match the lock file.
Each line below is a public API change. Review it, then re-run with
--update-conformance-lock to accept:
    AcceptCallResponse: (new model) -> decodable
    JoinCallResponse: codable -> decodable

Without this, a spec change could quietly move a public type from Codable to Decodable in a routine regeneration.

Two internal follow-ons. send<Response: Codable> becomes send<Response: Decodable>, and VideoEvent — the only model with a hand-written encode(to:) — loses those 5.6 KB of source along with its Encodable conformance.

⚠️ What breaks, and the open question

699 exported symbols are removed: 111 public types lose Encodable and JSONEncodable, 64 lose Decodable.

Containment pulls the highest-traffic types back to full Codable — CallResponse, CallSettingsResponse, GetCallResponse, OwnUserResponse, MemberResponse, CallSessionResponse, EgressResponse. Still stripped are all ~50 WebSocket event types (CallEndedEvent, CallRingEvent, CallSessionParticipantJoinedEvent, …) and most action responses (JoinCallResponse, GetOrCreateCallResponse, AcceptCallResponse). An app that logs events for analytics or persists a JoinCallResponse breaks on upgrade.

The open question for review: ALWAYS_CODABLE currently lists the 8 types Stream's own code and tests encode. That is a proxy for what apps encode, not a survey of it — and the four most important protected types are protected by containment, not by choice. If we want them protected deliberately, they should be listed explicitly so a spec change that reshuffles containment cannot drop them. Worth agreeing the policy before this merges.

Also worth noting: the keep-list took two rounds of compiler errors to find. The first build reported 11 errors naming 7 types; fixing those surfaced QueryCallsResponse in a file group the compiler had not reached. Swift batches errors per file group, so "fix what breaks" is iterative discovery. The rest were found by grepping every encode( site in the test targets.

🧪 Manual Testing Notes

  • Script reproduces this diff from pristine generator output. Restoring OpenApi/generated from develop and running Scripts/optimizeGeneratedCodeSize.py produces this branch's Models/ byte-for-byte.
  • Lock drift is caught. Verified by simulating a spec change; the script exits non-zero with the per-model diff above and leaves the tree untouched.
  • StreamVideoTests: 2,175 pass. Two failed in the parallel run (CallStateMachineStageJoinedStage_Tests.test_joiningTransition_ownCapabilitiesChanged_controllerReceivesUpdate, Call_IntegrationTests.test_audioRoom_participantRequestsSpeakPermission_hostRejects_participantCannotToggleMicrophone) and both pass in isolation — the suite's usual load-related flakiness.
  • No silent runtime change inside the SDK. The only dynamic as? Encodable cast is in SelectiveEncodable, whose conformers are exclusively Stream_Video_Sfu_* protobuf types that never hold OpenAPI models; AnyEncodable.init takes an existential and is compile-checked.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

🤖 Generated with Claude Code

Request models keep only Encodable, response models only Decodable, so the
compiler stops synthesising the half nobody calls. The split is derived from
the generated client - response types from what it decodes, request types from
what it encodes, each closed over stored-property types - and an
ALWAYS_CODABLE policy list pins the models an app is most likely to
round-trip, together with everything they contain.

Because a conformance is public API, the derived result is locked to
Scripts/generated-model-conformance.json. A spec change that would move a type
between Codable, Decodable and Encodable fails the codegen script with a
per-model diff instead of shipping silently.

Measured on Release/arm64/device, stripped, against develop: StreamVideo goes
from 10355.1 KB to 10012.4 KB, of which 262.7 KB is this change and the rest
is #1286 underneath it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@martinmitrevski
martinmitrevski requested a review from a team as a code owner September 17, 2026 20:22
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5f1ddc23-e036-4444-ad69-1c067374acf1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@martinmitrevski
martinmitrevski marked this pull request as draft September 17, 2026 20:27
@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamVideo 8.99 MB 8.68 MB -312 KB 🚀
StreamVideoSwiftUI 2.39 MB 2.39 MB 0 KB 🟢
StreamVideoUIKit 2.51 MB 2.51 MB 0 KB 🟢
StreamWebRTC 11.88 MB 11.88 MB 0 KB 🟢
StreamCore 0.92 MB 0.92 MB 0 KB 🟢

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

StreamVideo XCSize

Object Diff (bytes)
DefaultAPI.o -75480
VideoEvent.o -4708
ClientEvent.o -3654
CallSettingsRequest.o -3474
UserEventPayload.o -2850
Show 174 more objects
Object Diff (bytes)
CallFrameRecordingFrameReadyEvent.o -2158
JoinCallRequest.o -2158
GetCallRingStateResponse.o -2006
CallUserFeedbackSubmittedEvent.o -1994
CallRingEvent.o -1814
CallMissedEvent.o -1794
CallNotificationEvent.o -1794
CallRequest.o -1778
QueryCallsRequest.o -1722
CallMemberUpdatedPermissionEvent.o -1710
JoinCallResponse.o -1682
QueryMembersRequest.o -1670
CallStatsReportSummaryResponse.o -1630
AudioSettingsRequest.o -1618
QueryCallParticipantsResponse.o -1586
CallSessionParticipantCountsUpdatedEvent.o -1542
CallModerationWarningEvent.o -1538
CallRejectedEvent.o -1538
QueryCallStatsRequest.o -1526
GoLiveRequest.o -1514
UserRequest.o -1502
CallClosedCaption.o -1494
CallCreatedEvent.o -1490
CallMemberUpdatedEvent.o -1470
CallMemberAddedEvent.o -1470
LayoutSettings.o -1466
CallSessionParticipantLeftEvent.o -1458
CustomVideoEvent.o -1446
CollectUserFeedbackRequest.o -1438
CallAcceptedEvent.o -1438
CallUserMutedEvent.o -1434
CallEndedEvent.o -1430
PermissionRequestEvent.o -1430
UpdatedCallPermissionsEvent.o -1430
CallFrameRecordingStoppedEvent.o -1426
CallFrameRecordingStartedEvent.o -1426
CallRecordingStoppedEvent.o -1426
CallSessionStartedEvent.o -1426
CallFrameRecordingFailedEvent.o -1426
CallSessionEndedEvent.o -1426
CallRecordingStartedEvent.o -1426
AppUpdatedEvent.o -1414
BlockedUserEvent.o -1402
UpdateCallRequest.o -1402
CallSessionParticipantJoinedEvent.o -1390
CallUpdatedEvent.o -1386
UserUpdatedEvent.o -1386
RTMPBroadcastRequest.o -1378
CallMemberRemovedEvent.o -1362
KickedUserEvent.o -1358
IngressSettingsRequest.o -1354
UpdateCallResponse.o -1354
CallRecording.o -1346
VideoSettingsRequest.o -1346
CallTranscription.o -1346
UpdateCallMembersRequest.o -1330
MuteUsersRequest.o -1330
GetOrCreateCallResponse.o -1302
TranscriptionSettingsRequest.o -1298
GetOrCreateCallRequest.o -1294
UnblockedUserEvent.o -1290
CallReactionEvent.o -1290
ClosedCaptionEvent.o -1274
CallTranscriptionReadyEvent.o -1274
CallRecordingReadyEvent.o -1274
CallLiveStartedEvent.o -1270
CallDeletedEvent.o -1270
CallRtmpBroadcastStoppedEvent.o -1250
CallStatsReportReadyEvent.o -1250
CallRtmpBroadcastFailedEvent.o -1250
CallHLSBroadcastingStartedEvent.o -1250
CallRtmpBroadcastStartedEvent.o -1250
RecordSettingsRequest.o -1226
BroadcastSettingsRequest.o -1218
UpdateUserPermissionsRequest.o -1206
MemberRequest.o -1198
SendReactionRequest.o -1198
FrameRecordingSettingsRequest.o -1194
StopTranscriptionRequest.o -1182
DeleteCallRequest.o -1182
StopClosedCaptionsRequest.o -1182
ThumbnailsSettingsRequest.o -1182
StopLiveRequest.o -1178
ReactionResponse.o -1158
QueryMembersResponse.o -1142
QueryCallStatsResponse.o -1138
FileUploadConfig.o -1134
CallHLSBroadcastingFailedEvent.o -1130
CallClosedCaptionsFailedEvent.o -1130
CallHLSBroadcastingStoppedEvent.o -1130
CallClosedCaptionsStartedEvent.o -1130
CallClosedCaptionsStoppedEvent.o -1130
CallTranscriptionStoppedEvent.o -1130
CallTranscriptionStartedEvent.o -1130
CallTranscriptionFailedEvent.o -1130
CallRecordingFailedEvent.o -1130
IngressVideoEncodingOptionsRequest.o -1122
HLSSettingsRequest.o -1122
AppEventResponse.o -1110
ScreensharingSettingsRequest.o -1110
StartTranscriptionRequest.o -1098
StartClosedCaptionsRequest.o -1098
Credentials.o -1098
IngressVideoLayerRequest.o -1094
IngressAudioEncodingOptionsRequest.o -1094
RingCallRequest.o -1074
ICEServer.o -1054
RTMPSettingsRequest.o -1042
IngressSourceRequest.o -1030
RingSettingsRequest.o -1026
SortParamRequest.o -1018
LimitsSettingsRequest.o -1018
DeleteCallResponse.o -1010
QueryCallParticipantsRequest.o -1002
SendEventRequest.o -1002
CreateGuestResponse.o -994
BackstageSettingsRequest.o -994
ReportClientEventRequest.o -974
KickUserRequest.o -974
StartRTMPBroadcastsRequest.o -974
ListTranscriptionsResponse.o -970
ListRecordingsResponse.o -970
SFUResponse.o -966
EncryptionSettingsRequest.o -966
RingCallResponse.o -962
UpdateCallMembersResponse.o -958
PinRequest.o -942
UnpinRequest.o -942
RequestPermissionRequest.o -938
RawRecordingSettingsRequest.o -926
IndividualRecordingSettingsRequest.o -926
CreateGuestRequest.o -914
SendReactionResponse.o -910
GoLiveResponse.o -902
StopLiveResponse.o -902
StartRecordingRequest.o -890
RejectCallRequest.o -890
StartHLSBroadcastingResponse.o -890
StartFrameRecordingRequest.o -890
GeofenceSettingsRequest.o -882
BlockUserRequest.o -858
UnblockUserRequest.o -858
SessionSettingsRequest.o -838
StatsOptions.o -810
PinResponse.o -774
AcceptCallResponse.o -774
KickUserResponse.o -774
EndCallResponse.o -774
UnpinResponse.o -774
StopAllRTMPBroadcastsResponse.o -774
UpdateUserPermissionsResponse.o -774
DeleteTranscriptionResponse.o -774
StartFrameRecordingResponse.o -774
StartRTMPBroadcastsResponse.o -774
CollectUserFeedbackResponse.o -774
StopHLSBroadcastingResponse.o -774
StartClosedCaptionsResponse.o -774
StartTranscriptionResponse.o -774
StopClosedCaptionsResponse.o -774
StopFrameRecordingResponse.o -774
StopRTMPBroadcastsResponse.o -774
ReportClientEventResponse.o -774
StopTranscriptionResponse.o -774
RequestPermissionResponse.o -774
DeleteRecordingResponse.o -774
StartRecordingResponse.o -774
StopRecordingResponse.o -774
UnblockUserResponse.o -774
SendEventResponse.o -774
MuteUsersResponse.o -774
BlockUserResponse.o -774
CallSessionResponse.o +116
Encodable+Retroactive.o -104
Call.o +52

@github-actions

Copy link
Copy Markdown
1 Message
📖 Skipping Danger since the Pull Request is classed as Draft/Work In Progress

Generated by 🚫 Danger

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