auth/docs/KOTLIN_SIGNUP_FIX.md

7.1 KiB

Kotlin Signup API Response Fix

Problem

The error "Expected response body of the type 'class com.example.livingai...'" indicates that your Kotlin data class doesn't match the actual API response structure.

Actual API Response Structure

The /auth/signup endpoint returns:

{
  "success": true,
  "user": {
    "id": "uuid-here",
    "phone_number": "+919876543210",
    "name": "John Doe",
    "country_code": "+91",
    "created_at": "2024-01-15T10:30:00Z"
  },
  "access_token": "jwt-token",
  "refresh_token": "jwt-token",
  "needs_profile": true,
  "is_new_account": true,
  "is_new_device": true,
  "active_devices_count": 1,
  "location_id": "uuid-or-null"
}

Correct Kotlin Data Classes

Option 1: Using Gson/Moshi with @SerializedName

import com.google.gson.annotations.SerializedName
// OR for Moshi: import com.squareup.moshi.Json

data class SignupUser(
    val id: String,
    @SerializedName("phone_number") val phoneNumber: String,
    val name: String?,
    @SerializedName("country_code") val countryCode: String?,
    @SerializedName("created_at") val createdAt: String?
)

data class SignupResponse(
    val success: Boolean,
    val user: SignupUser,
    @SerializedName("access_token") val accessToken: String,
    @SerializedName("refresh_token") val refreshToken: String,
    @SerializedName("needs_profile") val needsProfile: Boolean,
    @SerializedName("is_new_account") val isNewAccount: Boolean,
    @SerializedName("is_new_device") val isNewDevice: Boolean,
    @SerializedName("active_devices_count") val activeDevicesCount: Int,
    @SerializedName("location_id") val locationId: String?
)

Option 2: Using Kotlinx Serialization

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SignupUser(
    val id: String,
    @SerialName("phone_number") val phoneNumber: String,
    val name: String? = null,
    @SerialName("country_code") val countryCode: String? = null,
    @SerialName("created_at") val createdAt: String? = null
)

@Serializable
data class SignupResponse(
    val success: Boolean,
    val user: SignupUser,
    @SerialName("access_token") val accessToken: String,
    @SerialName("refresh_token") val refreshToken: String,
    @SerialName("needs_profile") val needsProfile: Boolean,
    @SerialName("is_new_account") val isNewAccount: Boolean,
    @SerialName("is_new_device") val isNewDevice: Boolean,
    @SerialName("active_devices_count") val activeDevicesCount: Int,
    @SerialName("location_id") val locationId: String? = null
)

Option 3: Using Retrofit with Field Naming Strategy

If using Retrofit, you can configure it to handle snake_case automatically:

// In your Retrofit builder
val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(
        GsonConverterFactory.create(
            GsonBuilder()
                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                .create()
        )
    )
    .build()

// Then your data classes can use camelCase
data class SignupUser(
    val id: String,
    val phoneNumber: String,  // Will map to "phone_number"
    val name: String?,
    val countryCode: String?,  // Will map to "country_code"
    val createdAt: String?  // Will map to "created_at"
)

data class SignupResponse(
    val success: Boolean,
    val user: SignupUser,
    val accessToken: String,  // Will map to "access_token"
    val refreshToken: String,  // Will map to "refresh_token"
    val needsProfile: Boolean,
    val isNewAccount: Boolean,
    val isNewDevice: Boolean,
    val activeDevicesCount: Int,
    val locationId: String?
)

Complete Example with Error Handling

// SignupRequest.kt
data class SignupRequest(
    val name: String,
    @SerializedName("phone_number") val phoneNumber: String,
    val state: String? = null,
    val district: String? = null,
    @SerializedName("city_village") val cityVillage: String? = null,
    @SerializedName("device_id") val deviceId: String? = null,
    @SerializedName("device_info") val deviceInfo: Map<String, String?>? = null
)

// SignupResponse.kt
data class SignupUser(
    val id: String,
    @SerializedName("phone_number") val phoneNumber: String,
    val name: String?,
    @SerializedName("country_code") val countryCode: String?,
    @SerializedName("created_at") val createdAt: String?
)

data class SignupResponse(
    val success: Boolean,
    val user: SignupUser,
    @SerializedName("access_token") val accessToken: String,
    @SerializedName("refresh_token") val refreshToken: String,
    @SerializedName("needs_profile") val needsProfile: Boolean,
    @SerializedName("is_new_account") val isNewAccount: Boolean,
    @SerializedName("is_new_device") val isNewDevice: Boolean,
    @SerializedName("active_devices_count") val activeDevicesCount: Int,
    @SerializedName("location_id") val locationId: String?
)

// ErrorResponse.kt
data class ErrorResponse(
    val success: Boolean? = null,
    val error: String? = null,
    val message: String? = null,
    @SerializedName("user_exists") val userExists: Boolean? = null
)

// Usage in your ViewModel or Repository
suspend fun signup(
    name: String,
    phoneNumber: String,
    state: String? = null,
    district: String? = null,
    cityVillage: String? = null
): Result<SignupResponse> {
    return try {
        val request = SignupRequest(
            name = name,
            phoneNumber = phoneNumber,
            state = state,
            district = district,
            cityVillage = cityVillage,
            deviceId = getDeviceId(),
            deviceInfo = getDeviceInfo()
        )
        
        val response = apiClient.post<SignupResponse>("/auth/signup", request)
        
        if (response.isSuccessful && response.body()?.success == true) {
            Result.success(response.body()!!)
        } else {
            // Parse error response
            val errorBody = response.errorBody()?.string()
            val errorResponse = Gson().fromJson(errorBody, ErrorResponse::class.java)
            Result.failure(Exception(errorResponse.message ?: errorResponse.error ?: "Signup failed"))
        }
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Common Issues and Fixes

Issue 1: Field Name Mismatch

Problem: API uses snake_case but Kotlin uses camelCase Fix: Use @SerializedName annotation or configure Retrofit with field naming policy

Issue 2: Nullable Fields

Problem: Some fields might be null (like location_id) Fix: Mark optional fields as nullable: val locationId: String?

Issue 3: Nested Objects

Problem: user is a nested object Fix: Create separate data class for SignupUser

Issue 4: Type Mismatch

Problem: API returns "success": true but expecting different type Fix: Use Boolean type: val success: Boolean

Testing the Response

Add logging to see the actual response:

val response = apiClient.post("/auth/signup", request)
Log.d("Signup", "Response code: ${response.code()}")
Log.d("Signup", "Response body: ${response.body()?.string()}")

This will help you see exactly what the API is returning and adjust your data class accordingly.