Store booleans as JSON booleans in the Realtime Database on Apple targets - #840
Draft
kihaki wants to merge 3 commits into
Draft
Store booleans as JSON booleans in the Realtime Database on Apple targets#840kihaki wants to merge 3 commits into
kihaki wants to merge 3 commits into
Conversation
…gets The Firebase Apple SDKs only serialize an NSNumber as a JSON boolean if it is a CFBoolean singleton. A Kotlin Boolean crossing the interop boundary is boxed as a different NSNumber subclass, and Kotlin/Native bridges any CFBoolean entering Kotlin back to a Kotlin Boolean, so a CFBoolean can never be held in Kotlin code. Booleans written from Apple targets therefore end up as 1/0 in the database (GitLiveApp#275, GitLiveApp#667), which also crashes Android readers that map them into Boolean fields. Fix: before handing an encoded value to the FirebaseDatabase SDK, rebuild boolean-carrying containers as Foundation objects by serializing them to JSON text (where boolean identity is still known) and parsing with NSJSONSerialization. The parsed result is a lazy Foundation-backed view, so the CFBooleans inside never surface to Kotlin and reach the SDK intact. A boolean at the root is wrapped in the SDK-supported {".value": x} leaf form. Containers without booleans and values JSON cannot represent are passed through unchanged.
Boolean startAt/endAt/equalTo previously sent the Kotlin Boolean boxed as a numeric NSNumber, which happened to match the numeric writes. With writes fixed they must send CFBooleans too, or boolean queries silently stop matching. A scalar cannot ride in a JSON container, so a small cinterop shim receives the bound as a primitive C BOOL and applies the query selector on the ObjC side, where the CFBoolean never surfaces in Kotlin. Floats inside transformed containers are widened to Double before writing the text form, so they parse to the same Double the directly bridged NSNumber produced and boolean presence never changes adjacent numbers. New emulator tests assert raw snapshot values, which distinguish stored booleans from stored 0/1 where the typed decoder deliberately does not: direct set, nested set, updateChildren, transactions, both onDisconnect variants, and boolean query bounds against neighboring numeric 0/1 nodes.
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.
Problem
Booleans written to the Realtime Database from Apple targets are stored as
1/0instead oftrue/false(#667, previously #275). Android and JVM clients that map such nodes intoBooleanfields then crash withFailed to convert value of type java.lang.Long to boolean. #275 was closed by #549, but that PR only fixed decoding (reading1/0back as aBoolean), the written data is still numeric.Root cause
The Firebase Apple SDKs only serialize an
NSNumberas a JSON boolean when it is one of the CFBoolean singletons (kCFBooleanTrue/kCFBooleanFalse, the instances behindNSNumber(bool:)). A KotlinBooleancrossing the interop boundary is boxed by Kotlin/Native as its ownNSNumbersubclass, which is not a CFBoolean, so it serializes by numeric value. JetBrains considers this intended behavior (JetBrains/kotlin-native#3857).Fixing this inside the encoder is impossible: Kotlin/Native bridges in both directions, so a CFBoolean can never be held in Kotlin code. Verified on Kotlin 2.2:
Fix
Writes. The one place a CFBoolean survives is inside a Foundation container that Kotlin never unwraps.
NSJSONSerializationparsing produces exactly that: the result is a lazy Foundation-backed view (NSDictionaryAsKMap), and the CFBooleans inside it reach the SDK untouched. New appleMain helperwithFoundationBooleans()infirebase-common-internalserializes a boolean-carrying container to JSON text (in Kotlin, where boolean identity is still known) and parses it back withNSJSONSerialization.firebase-databaseapplies it at every value hand-off:setValue,updateChildValues, bothonDisconnectvariants, and transaction results. A boolean at the root (setValue(true)) cannot survive on its own, so it is wrapped in the SDK-supported{".value": x}leaf form (kPayloadValueinFSnapshotUtilities.m), which resolves to a plain scalar write.Floatvalues inside transformed containers are widened toDoublebefore writing the text form, so they parse to the sameDoublethe directly bridged NSNumber produced (1.2fstays1.2000000476837158) and boolean presence never changes adjacent numeric data.Queries.
startAt/endAt/equalTo(Boolean)previously sent the boxed number too, which happened to match the numeric writes. With writes fixed, the bounds must be real CFBooleans as well or boolean queries would silently stop matching. A scalar cannot ride in a JSON container, so this uses a small cinterop shim (booleanQuery.def): the boolean enters ObjC as a primitive CBOOL(unaffected by NSNumber boxing), the shim selectskCFBooleanTrue/kCFBooleanFalseand invokes the query selector viaperformSelector, and the CFBoolean never surfaces in Kotlin. The shim is typedidso it needs no FirebaseDatabase headers.Behavior is unchanged for anything the round-trip cannot represent: boolean-free containers are returned identically (fast path), and containers holding custom objects, non-String keys, or non-finite doubles fall back to the previous behavior.
Not covered
Firestore has the same underlying issue but a non-JSON value model (Timestamp, GeoPoint, DocumentReference), so this PR is scoped to the Realtime Database. The helper intentionally lives in
firebase-common-internalso a Firestore fix can reuse the technique with its own traversal.Tests
FoundationValueTest(firebase-common-internalappleTest, runs without pods) asserts viaNSJSONSerializationoutput, the same Foundation serialization the SDK uses:mapOf("v" to true)serializes as{"v":1}(the bug)true/false1.2fkeeps its bridged widening1.20000004768371582^53+1), strings with escapes, and ServerValue sentinels are preservedFirebaseDatabaseTest(commonTest, against the emulator) gains raw-snapshot assertions that distinguish stored booleans from stored0/1(the typed decoder deliberately accepts both, so the existing typed tests could not catch this):updateChildren, transactiononDisconnect().setValueandonDisconnect().updateChildrenorderByValue().equalTo/startAt/endAt(Boolean)against a node that also contains the numbers0and1, so a numeric bound would return the wrong childrenVerified locally:
firebase-common-internaliosSimulatorArm64 + macosArm64 test suites,firebase-databaseiosSimulatorArm64 emulator suite (19/19) and jvm emulator suite (19/19), kotlinter clean.Fixes #667. Relates to #275 / #549.