fix: serialize booleans as native JSON in precomputed request body#243
fix: serialize booleans as native JSON in precomputed request body#243leoromanovsky merged 5 commits intomainfrom
Conversation
The buildRequestBody() method was converting all non-numeric attribute values to strings, including booleans. This caused boolean attributes like "isPremium: true" to be sent as "isPremium: \"true\"" (string) instead of "isPremium: true" (native JSON boolean). This fix aligns the Android SDK with the JS and iOS SDKs, which preserve native boolean types in the wire format. Changes: - Add ContextAttributesSerializer utility for consistent serialization - Properly handle booleans as native JSON booleans in categorical attrs - Exclude null values from serialization (matching JS/iOS behavior) - Add comprehensive unit tests for wire format verification - Add precomputed-v1.json test data for integration tests
The precomputed-v1.json file should be fetched via `make test-data` rather than committed to the repository.
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where boolean attributes were incorrectly serialized as strings ("true", "false") instead of native JSON booleans (true, false) in the precomputed request body. This aligns the Android SDK's wire format with the JS and iOS SDKs.
Changes:
- Introduced
ContextAttributesSerializerutility for consistent attribute serialization across subject and bandit action attributes - Updated serialization to preserve booleans as native JSON types and exclude null values
- Version bumped to 4.12.1
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| eppo/src/main/java/cloud/eppo/android/util/ContextAttributesSerializer.java | New utility class that properly handles boolean serialization as native JSON types |
| eppo/src/main/java/cloud/eppo/android/EppoPrecomputedClient.java | Refactored to use the new serializer utility, removing duplicated serialization logic |
| eppo/src/test/java/cloud/eppo/android/ContextAttributesSerializerTest.java | Comprehensive unit tests verifying wire format correctness |
| eppo/build.gradle | Version bump to 4.12.1 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
eppo/src/test/java/cloud/eppo/android/ContextAttributesSerializerTest.java
Outdated
Show resolved
Hide resolved
…zerTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
aarsilv
left a comment
There was a problem hiding this comment.
Good catch and glad we have a shared test case now that exercises this.
Approving with two minor comments you can take or leave:
- The new
EppoValue.unwrap()method may be useful - Consideration of
null-valued attributes
| if (attributes != null) { | ||
| for (String key : attributes.keySet()) { | ||
| EppoValue value = attributes.get(key); | ||
| if (value != null && !value.isNull()) { |
There was a problem hiding this comment.
Should we be adding null as the value if it is null?
There was a problem hiding this comment.
We explicitly skip null in swift and javascript so I was matching that behavior.
| for (String key : attributes.keySet()) { | ||
| EppoValue value = attributes.get(key); | ||
| if (value != null && !value.isNull()) { | ||
| if (value.isNumeric()) { |
There was a problem hiding this comment.
I wonder if the newly-added unrwap() could help simplify things here.
There was a problem hiding this comment.
Thanks, good suggestion, hadn't explore the common java repo.
There was a problem hiding this comment.
It doesn't simplify the code for us because we need to know the variation type, would look like this:
// We'd still need the same if/else chain to pick the VariationType
if (value.isNumeric()) {
numericAttrs.put(key, value.unwrap(VariationType.NUMERIC)); // Double
} else if (value.isBoolean()) {
categoricalAttrs.put(key, value.unwrap(VariationType.BOOLEAN)); // Boolean
} else {
categoricalAttrs.put(key, value.unwrap(VariationType.STRING)); // String
}
Is that what you were trying to simplify?
There was a problem hiding this comment.
It would be more helpful to augment the EppoValue in jdk common to auto detect the type:
public Object unwrap() {
switch (this.type) {
case BOOLEAN: return boolValue;
case NUMBER: return doubleValue;
case STRING: return stringValue;
...
}
}
Then the serializer becomes:
if (value.isNumeric()) {
numericAttrs.put(key, (Number) value.unwrap());
} else {
categoricalAttrs.put(key, value.unwrap());
}
| } | ||
|
|
||
| @Test | ||
| public void testBooleansNotConvertedToStrings() { |
| * by the precomputed flags API and to maintain consistency with other Eppo SDKs (JS, iOS). | ||
| */ | ||
| @RunWith(RobolectricTestRunner.class) | ||
| public class ContextAttributesSerializerTest { |
There was a problem hiding this comment.
I like that we pulled this out into it's own testable class. Since we're doing that, let's include a test for null valued attributes to cover that base.
Cover both EppoValue.nullValue() and Java null in the attributes map to document that both are excluded from serialization output.
Motivation
The
buildRequestBody()method inEppoPrecomputedClientwas converting all non-numeric attribute values to strings, including booleans. This caused boolean attributes to be sent in the wrong wire format:Before (incorrect):
{ "categoricalAttributes": { "isPremium": "true", "hasPushEnabled": "false" } }After (correct):
{ "categoricalAttributes": { "isPremium": true, "hasPushEnabled": false } }This fix aligns the Android SDK with the JS SDK and iOS SDK (see ios-sdk#95), which preserve native boolean types in the wire format.
The shared test data in
sdk-test-data/configuration-wire/precomputed-v1.jsonconfirms that booleans should be native JSON booleans:Changes
ContextAttributesSerializerutility class for consistent attribute serializationbuildRequestBody()by using the new utilityprecomputed-v1.jsontest data for integration testsDecisions
ContextAttributesSerializer) for testability and reuse across subject attributes and bandit actionsMap<String, Object>for categorical attributes instead ofMap<String, String>to allow native boolean valuesTest plan
./gradlew :eppo:testDebugUnitTest./gradlew :eppo:connectedDebugAndroidTest