diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
index 92422702..eb67a0eb 100644
--- a/.github/badges/jacoco.svg
+++ b/.github/badges/jacoco.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9f77082..d1f68eae 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 447b8538..38caed6a 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 0acec103..e5db9c24 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