Skip to content

Conversation

@swolfand
Copy link
Collaborator

@swolfand swolfand commented Nov 10, 2025

Summary of changes

Adds support for setting unsafeMetadata when updating a user in the Clerk-Android SDK. This change introduces the unsafeMetadata field to User.UpdateParams, aligning the Android SDK with the existing functionality in the Clerk JS SDK.


Linear Issue: MOBILE-314

Open in Cursor Open in Web

Summary by CodeRabbit

  • New Features

    • Added an "unsafe metadata" field to user accounts, enabling storage and updates of arbitrary JSON-formatted metadata on profiles.
  • Tests

    • Added tests verifying inclusion/exclusion, JSON serialization, empty-value handling, and preservation of nested/complex unsafe metadata during updates.

@cursor
Copy link

cursor bot commented Nov 10, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@swolfand swolfand marked this pull request as ready for review November 10, 2025 20:31
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

📝 Walkthrough

Walkthrough

Adds an optional unsafeMetadata: String? property (serialized as "unsafe_metadata") to the User data class and its nested UpdateParams in the Android SDK, enabling unsafe metadata to be supplied as a JSON string during user updates. Adds a new test class UserTest validating UpdateParams.toMap() behavior and JSON serialization for various unsafeMetadata cases (present, null, empty, complex nested JSON) alongside existing metadata fields.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

  • Check that @SerialName("unsafe_metadata") is used consistently on both User and UpdateParams.
  • Confirm String? is the intended payload type for unsafe metadata (JSON string assumption).
  • Verify UpdateParams.toMap() logic includes/excludes the field correctly for null/empty values.
  • Review new unit tests for coverage and correctness.

Possibly related issues

  • MOBILE-314: Android - Add support for setting unsafe metadata in Clerk-Android — This change implements adding unsafeMetadata to the SDK model and update params as requested.
  • clerk/clerk-android issue 352 — The PR directly modifies User and User.UpdateParams to add an unsafeMetadata field, aligning with that issue's objective.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add unsafe metadata support to clerk-android' clearly and concisely summarizes the main change—adding unsafeMetadata field support to the User update functionality.
Description check ✅ Passed The description follows the required template with a 'Summary of changes' section that explains the feature, links the Linear issue, and provides context about alignment with the Clerk JS SDK.
Linked Issues check ✅ Passed The implementation adds unsafeMetadata field to User and UpdateParams classes with comprehensive tests, fulfilling MOBILE-314's objective to support setting unsafe metadata on users.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing unsafeMetadata support: field additions to User and UpdateParams data classes, plus comprehensive unit tests for the new functionality.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6ab82ad and 1b68bf4.

📒 Files selected for processing (1)
  • source/api/src/test/java/com/clerk/api/user/UserTest.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*

⚙️ CodeRabbit configuration file

If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes.

Files:

  • source/api/src/test/java/com/clerk/api/user/UserTest.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (9)
source/api/src/test/java/com/clerk/api/user/UserTest.kt (9)

1-10: LGTM: Clean imports and package structure.

The imports are appropriate for testing Android SDK components with kotlinx.serialization and Robolectric.


12-13: LGTM: Proper test runner configuration.

Robolectric is correctly configured for Android unit testing.


15-30: LGTM: Well-structured inclusion test.

The test properly verifies that unsafeMetadata is included in the map with the correct snake_case key unsafe_metadata when provided.


32-48: LGTM: Proper null handling test.

Correctly verifies that null unsafeMetadata is excluded from the map rather than included with a null value, which is the expected API behavior.


50-78: LGTM: Comprehensive metadata types test.

Excellent coverage verifying that all three metadata types (public, private, unsafe) can be used together without conflicts.


80-94: LGTM: Good isolation test.

The map size assertion on line 92 is particularly valuable—it ensures that only the provided field appears in the map, with no unexpected entries.


96-111: LGTM: JSON escaping correctly verified.

The assertion on line 109 properly addresses the previous review concern by checking for the escaped version of the JSON string (\"custom\":\"data\"), which is how kotlinx.serialization correctly serializes a String field containing JSON according to the JSON specification.


113-129: LGTM: Empty metadata edge case handled.

Correctly verifies that an empty JSON object "{}" is preserved rather than being treated as null or filtered out.


131-148: LGTM: Complex nested structure preserved.

Excellent test case verifying that deeply nested JSON structures are preserved without modification, which is critical for real-world usage where unsafeMetadata may contain complex data.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd6d87 and 910e252.

📒 Files selected for processing (1)
  • source/api/src/main/kotlin/com/clerk/api/user/User.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*

⚙️ CodeRabbit configuration file

If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes.

Files:

  • source/api/src/main/kotlin/com/clerk/api/user/User.kt

Comment on lines +196 to +197
/** JSON string containing unsafe metadata to update. */
@SerialName("unsafe_metadata") val unsafeMetadata: String? = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Implementation looks good and follows existing patterns.

The addition of unsafeMetadata to UpdateParams correctly follows the same pattern as publicMetadata and privateMetadata. The type String? is appropriate for the update parameters, matching the existing metadata fields.

However, the AI summary incorrectly states that unsafeMetadata was added to the User data class. The field already exists at line 165, so this appears to be a documentation inconsistency rather than a code change.

As per coding guidelines, please add tests to cover the new unsafeMetadata parameter. The tests should verify:

  1. Updating a user with unsafeMetadata succeeds
  2. The field is properly serialized in API requests
  3. The updated metadata is correctly reflected in the returned User object

Would you like me to help generate test cases for this functionality?

Additionally, verify that the @AutoMap annotation properly handles the new field:


🏁 Script executed:

#!/bin/bash
# Description: Verify that unsafeMetadata is properly integrated with the UpdateParams serialization

# Search for how UpdateParams.toMap() is implemented or generated by @AutoMap
rg -n "toMap" --type=kotlin -A 5 -B 2

# Search for any existing tests of UpdateParams to understand the test pattern
rg -n "UpdateParams" --type=kotlin -g "*test*" -C 3

# Search for any existing usage of publicMetadata or privateMetadata updates to see the pattern
rg -n "publicMetadata|privateMetadata" --type=kotlin -g "**/*test*" -C 3

Length of output: 41447



---

🏁 Script executed:

```shell
#!/bin/bash
# Better search for test files related to User.kt and UpdateParams

# Search for test files by common naming patterns
echo "=== Searching for User-related test files ==="
fd -e kt -path "*test*" | grep -i user | head -20

echo -e "\n=== Searching for UpdateParams in any Kotlin files (broader search) ==="
rg -n "UpdateParams" --type=kotlin -B2 -A2 | head -50

echo -e "\n=== Checking for test directory structure ==="
find . -type d -name "test" -o -name "tests" 2>/dev/null | head -10

echo -e "\n=== Search for any test involving metadata updates ==="
rg -n "metadata" --type=kotlin -g "*test*" -C2 | head -40

Length of output: 3857



🏁 Script executed:

#!/bin/bash
# Search in test directory specifically

echo "=== List files in test directory ==="
find ./source/api/src/test -type f -name "*.kt" 2>/dev/null | head -20

echo -e "\n=== Search for User in test files ==="
find ./source/api/src/test -type f -name "*.kt" -exec grep -l "User" {} \; 2>/dev/null

echo -e "\n=== Search for update or Update in test files ==="
find ./source/api/src/test -type f -name "*.kt" -exec grep -l "update\|Update" {} \; 2>/dev/null | head -10

echo -e "\n=== Check for any metadata tests ==="
find ./source/api/src/test -type f -name "*.kt" | xargs grep -l "metadata" 2>/dev/null

Length of output: 1622



🏁 Script executed:

#!/bin/bash
# Check what User-related tests exist in ClerkTest.kt

echo "=== Inspect ClerkTest.kt for update-related tests ==="
grep -n "update\|Update" ./source/api/src/test/java/com/clerk/api/sdk/ClerkTest.kt

echo -e "\n=== Check if ClerkTest.kt tests User.update() ==="
grep -n -A5 -B5 "User\|user" ./source/api/src/test/java/com/clerk/api/sdk/ClerkTest.kt | head -50

echo -e "\n=== Verify the PR changes - check if any test files were modified ==="
# Look at the actual file structure to confirm no test changes
find . -name "*.kt" -path "*test*" -newer ./source/api/src/main/kotlin/com/clerk/api/user/User.kt 2>/dev/null | head -5 || echo "Cannot determine file modification times"

Length of output: 2795


Add tests for the new unsafeMetadata field in User.update().

The implementation correctly follows the existing pattern for publicMetadata and privateMetadata fields, with proper @AutoMap and @SerialName annotations. However, per coding guidelines, tests must be added to cover this new functionality since none exist in the codebase for User.update() operations.

Add tests to verify:

  1. Updating a user with unsafeMetadata succeeds
  2. The field is properly serialized in API requests via toMap()
  3. Updated metadata is correctly reflected in the returned User object

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
source/api/src/test/java/com/clerk/api/user/UserTest.kt (1)

113-148: Good edge case coverage for empty and complex metadata.

The tests properly validate that empty JSON objects and complex nested structures are preserved correctly.

Consider adding optional tests for additional edge cases:

@Test
fun `UpdateParams with invalid JSON should pass through to server validation`() {
  // Given - invalid JSON
  val invalidJson = """{"incomplete":"""
  val params = User.UpdateParams(unsafeMetadata = invalidJson)
  
  // When
  val paramsMap = params.toMap()
  
  // Then - should preserve the string even if invalid
  assertEquals("Should preserve even invalid JSON", invalidJson, paramsMap["unsafe_metadata"])
}

@Test
fun `UpdateParams with empty string unsafeMetadata should be handled`() {
  // Given
  val params = User.UpdateParams(unsafeMetadata = "")
  
  // When
  val paramsMap = params.toMap()
  
  // Then
  assertEquals("Should include empty string", "", paramsMap["unsafe_metadata"])
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 910e252 and 6ab82ad.

📒 Files selected for processing (1)
  • source/api/src/test/java/com/clerk/api/user/UserTest.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*

⚙️ CodeRabbit configuration file

If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes.

Files:

  • source/api/src/test/java/com/clerk/api/user/UserTest.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
source/api/src/test/java/com/clerk/api/user/UserTest.kt (4)

15-48: Excellent coverage of inclusion and exclusion behavior.

The tests properly validate that unsafeMetadata is included when provided and excluded when null, with correct key mapping to unsafe_metadata in the map representation.


50-94: Thorough validation of metadata field combinations.

These tests properly verify that multiple metadata types can coexist and that unsafeMetadata can be used independently. The size assertion in the solo test is particularly valuable for catching unintended field inclusions.


1-149: Comprehensive test coverage for the unsafe metadata feature.

The test suite provides excellent coverage of the new unsafeMetadata functionality, including:

  • Basic inclusion/exclusion behavior
  • Interaction with other metadata fields
  • JSON serialization
  • Edge cases (empty and complex structures)

The tests follow good practices with clear Given-When-Then structure and descriptive assertion messages.


12-12: Consider removing Robolectric from UserTest.kt if not required project-wide.

The test code demonstrates no Android-specific functionality. UpdateParams is a pure Kotlin data class, and the test uses only JUnit assertions and kotlinx.serialization. The toMap() method is generated by the @AutoMap annotation processor and doesn't require Android emulation.

However, Robolectric is used consistently across multiple test files in the project. Verify:

  1. Whether this is a project-wide testing convention or infrastructure requirement
  2. If removing it from UserTest.kt aligns with the project's testing strategy
  3. Whether any other tests or build configuration depend on this runner

If not required, use @RunWith(JUnit4::class) or the default runner to reduce test overhead and improve execution speed.

Comment on lines 96 to 111
@Test
fun `UpdateParams serialization should include unsafe_metadata field`() {
// Given
val unsafeMetadataJson = """{"custom":"data"}"""
val params = User.UpdateParams(username = "testuser", unsafeMetadata = unsafeMetadataJson)

// When
val json = Json.encodeToString(params)

// Then
assertTrue("Serialized JSON should contain unsafe_metadata", json.contains("unsafe_metadata"))
assertTrue(
"Serialized JSON should contain the metadata value",
json.contains(unsafeMetadataJson),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify JSON escaping in serialization test.

The assertion on lines 107-110 checks if the serialized JSON contains the unescaped unsafeMetadataJson string. However, when kotlinx.serialization encodes a String field containing JSON, it should escape the quotes. For example:

  • Input: unsafeMetadata = """{"custom":"data"}"""
  • Expected serialized: {"username":"testuser","unsafe_metadata":"{\"custom\":\"data\"}"}

The test json.contains(unsafeMetadataJson) might fail because it's searching for the unescaped string {"custom":"data"} within escaped JSON.

Please run this test to confirm it passes, or consider updating the assertion to verify proper JSON escaping:

If the test passes, the assertion is working as intended. If it fails, consider this alternative assertion:

// Verify proper JSON escaping
assertTrue(
  "Serialized JSON should contain escaped metadata value",
  json.contains("\\\"custom\\\":\\\"data\\\"") || json.contains("{\\\"custom\\\":\\\"data\\\"}")
)

🏁 Script executed:

#!/bin/bash
# Run the specific serialization test to verify it passes
./gradlew :api:testDebugUnitTest --tests "com.clerk.api.user.UserTest.UpdateParams serialization should include unsafe_metadata field"

Length of output: 189



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the User.UpdateParams class definition
rg -n "class UpdateParams|data class UpdateParams" --type=kt source/api/src

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Search for unsafeMetadata field definition and usage
rg -n "unsafeMetadata" --type=kt source/api/src -C 3

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check if there are custom serializers for UpdateParams
rg -n "@Serializable|KSerializer" --type=kt source/api/src -C 2 | grep -A 5 -B 5 "UpdateParams"

Length of output: 26



---

🏁 Script executed:

```shell
#!/bin/bash
# Find User.kt file in the codebase
fd -e kt User.kt source/api/src

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Search for UpdateParams class without type restriction
rg -n "UpdateParams" source/api/src -C 5 | head -100

Length of output: 8221


🏁 Script executed:

#!/bin/bash
# Search for unsafeMetadata without type restriction
rg -n "unsafeMetadata" source/api/src -C 3 | head -100

Length of output: 8692



---

🏁 Script executed:

```shell
#!/bin/bash
# View the User.kt file to find UpdateParams definition
cat source/api/src/main/kotlin/com/clerk/api/user/User.kt

Length of output: 26371



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for @AutoMap annotation and toMap method references
rg -n "toMap|AutoMap" source/api/src -C 3 | head -80

Length of output: 6826


🏁 Script executed:

#!/bin/bash
# Check for custom serializers or Json configuration
rg -n "kotlinx.serialization|Json\\.encodeToString|KSerializer" source/api/src -C 2 | head -60

Length of output: 4541



---

🌐 Web query:

```web
kotlinx.serialization String field JSON escaping behavior quote characters

💡 Result:

Short answer: kotlinx.serialization follows the JSON spec — string values are emitted in double quotes and any interior double-quote characters are escaped with a backslash (" ) when you serialize (e.g., Json.encodeToString). If you need an unquoted/raw JSON literal (not ordinary JSON string escaping), you can use JsonUnquotedLiteral (experimental) but that can produce invalid JSON if misused. [1][2][3]

Sources:

  • Json.encodeToString behavior (kotlinx.serialization docs). [1]
  • JsonUnquotedLiteral (creates unquoted/raw literal). [2]
  • Example showing library escaping double quotes and other characters when encoding strings. [3]

Fix the JSON escaping assertion in the serialization test.

The assertion at lines 107-110 checks for an unescaped JSON substring, but kotlinx.serialization escapes double quotes when serializing String fields per the JSON specification. When unsafeMetadata = """{"custom":"data"}""" is serialized, it becomes "{\"custom\":\"data\"}" in the JSON output, so the substring {"custom":"data"} will not be found.

Update the assertion to verify the escaped version:

assertTrue(
  "Serialized JSON should contain escaped metadata value",
  json.contains("\\\"custom\\\":\\\"data\\\"") || json.contains("{\\\"custom\\\":\\\"data\\\"}")
)
🤖 Prompt for AI Agents
In source/api/src/test/java/com/clerk/api/user/UserTest.kt around lines 96 to
111, the test asserts the raw JSON substring {"custom":"data"} which won't
appear because kotlinx.serialization escapes quotes in string fields; update the
assertion (lines ~107-110) to check for the escaped form of the metadata value
(e.g., look for \"custom\":\"data\" or include the surrounding escaped braces
{\"custom\":\"data\"}) so the test verifies the serialized, escaped JSON string
instead of the unescaped substring.

@swolfand swolfand merged commit 476e737 into main Nov 10, 2025
5 of 6 checks passed
@swolfand swolfand deleted the cursor/MOBILE-314-add-unsafe-metadata-support-to-clerk-android-4fb6 branch November 10, 2025 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants