feat: SyncJSON — ship the Sendable JSON payload carrier in the library - #672
Merged
Conversation
sync(...) runs off the main actor, so a payload crossing into it must be Sendable — which [String: Any] is not. Consumers were left to hand-roll a Sendable JSON box (the demo had DemoSyncPayload + DemoSyncValue, ~120 lines). Ship it once in the library instead: - SyncJSON: a Sendable enum JSON value conforming to SyncPayloadConvertible, with init(dictionary:), toSyncPayloadDictionary(), and keyed accessors (string/objectArray). NSNumber matched first so JSON bools aren't read as 1. Preserves null (so a sync can clear) and value shapes. - Demo drops DemoSyncPayload/DemoSyncValue entirely and uses SyncJSON. red-first: SyncJSONTests (round-trip, accessors, syncs as a payload). All green: SwiftSync 178, DemoCore 43, DemoBackend 32. README documents it.
3lvis
marked this pull request as ready for review
June 27, 2026 08:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
sync(...)runs off the main actor, so a payload crossing into it must beSendable. A raw[String: Any]isn't — so any consumer that decodes JSON on one actor and syncs it on another has to hand-roll aSendableJSON box. The demo had exactly that:DemoSyncPayload+DemoSyncValue, ~120 lines of generic boilerplate every consumer would otherwise reinvent.This ships that carrier once, in the library.
What
SyncJSON— aSendableenum JSON value conforming toSyncPayloadConvertible, so it feedssync(payload:)/sync(item:)directly.init(dictionary:),toSyncPayloadDictionary(), and keyed accessors (string(_:),objectArray(_:)).NSNumberis matched first so a JSON boolean isn't read as1(the classic bridging trap). Preservesnull(so a sync can clear a field) and the value shapes.DemoSyncPayload/DemoSyncValueand usesSyncJSONthroughout — net deletion of the duplicated box.Why this one earns its place (unlike the recent detours)
It has a present consumer (the demo adopts it in the same PR) and removes real duplication every consumer would hit — not speculative surface. It's the genuine library-ergonomics gap we traced from the
DemoSyncPayloaddiscussion: SwiftSync made consumers BYO-Sendable-JSON; now it doesn't.Verification
Red-first
SyncJSONTests(round-trip, keyed accessors, syncs as aSyncPayloadConvertible). All green: SwiftSync 178, DemoCore 43, DemoBackend 32. README documents the type.