From 70c772dfb1991b631d03491c2665476bd183069f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:27:46 +0200 Subject: [PATCH 1/2] fix: convert a JSON null to null, not to the text "null" `JsonElement.toPassableValue` matched `JsonNull` against its `is JsonPrimitive` branch, where every check failed and it fell to the catch-all that reads `content` - which for a null is the string "null". An audience filter asking `field == null` never matched, while `field == "null"` did. The sibling converter `convertFromJsonElement` already handles this, so the two now agree. Two existing assertions in JsonElementToPassableValueTest pinned the old behaviour and are updated. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + .../rule_logic/cel/SuperscriptEvaluator.kt | 6 ++++++ .../rule_logic/JsonElementToPassableValueTest.kt | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f770821..d1f68eae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw ## Unreleased - Fix prices not showing when product/offers are fetched from cache +- Fix a JSON null in placement parameters or user attributes reaching audience filters as the text `"null"`, so a filter checking whether a field is null never matched. ## 2.8.2 diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt index 447b85380..38caed6a5 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean @@ -246,6 +247,11 @@ internal fun Any.toPassableValue(): PassableValue = private fun JsonElement.toPassableValue(): PassableValue = when (this) { + // Checked before JsonPrimitive, which JsonNull is a kind of. Without this a + // JSON null falls past every primitive branch and comes out as the string + // "null", so an audience filter comparing the field to null never matches. + is JsonNull -> PassableValue.NullValue + is JsonObject -> PassableValue.MapValue( this.mapValues { (_, value) -> value.toPassableValue() }.toMap(), diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/JsonElementToPassableValueTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/JsonElementToPassableValueTest.kt index 0acec103a..e5db9c248 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/JsonElementToPassableValueTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/JsonElementToPassableValueTest.kt @@ -64,6 +64,14 @@ class JsonElementToPassableValueTest { assertEquals(false, (passableFalseValue as PassableValue.BoolValue).value) } + @Test + fun `test JsonNull conversion to PassableValue`() { + // JsonNull is a JsonPrimitive, so without an explicit branch it falls past + // every primitive check and comes out as the string "null". An audience + // filter asking `field == null` would then never match. + assertTrue(JsonNull.toPassableValue() is PassableValue.NullValue) + } + @Test fun `test JsonObject conversion to PassableValue`() { val jsonObject = @@ -94,8 +102,7 @@ class JsonElementToPassableValueTest { assertTrue(resultMap["boolean"] is PassableValue.BoolValue) assertEquals(true, (resultMap["boolean"] as PassableValue.BoolValue).value) - assertTrue(resultMap["null"] is PassableValue.StringValue) - assertEquals("null", (resultMap["null"] as PassableValue.StringValue).value) + assertTrue(resultMap["null"] is PassableValue.NullValue) } @Test @@ -128,8 +135,7 @@ class JsonElementToPassableValueTest { assertTrue(resultList[3] is PassableValue.BoolValue) assertEquals(true, (resultList[3] as PassableValue.BoolValue).value) - assertTrue(resultList[4] is PassableValue.StringValue) - assertEquals("null", (resultList[4] as PassableValue.StringValue).value) + assertTrue(resultList[4] is PassableValue.NullValue) } @Test From 23ed5145c9726fc0a9534297f52cf24d3af47c8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Sep 2026 13:46:37 +0000 Subject: [PATCH 2/2] Update coverage badge [skip ci] --- .github/badges/jacoco.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 924227022..eb67a0eb4 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -1 +1 @@ -coverage45.8% \ No newline at end of file +coverage45.7% \ No newline at end of file