8

After configuring Kotlin for Android project, I wrote a simple MainActivity.kt. It called Retrofit to get a JSON file which contained the following data:

{
    "message": "success",
    "user": {
        "username": "Eric"
    }
}

Now I want to use Moshi to convert the JSON data to Kotlin's class, so here are the two classes to reflect the above JSON structure:

class User(var username: String)

class UserJson(var message: String, var user: User)

And a custom type adapter for Moshi:

class UserAdapter {
    @FromJson fun fromJson(userJson: UserJson) : User {
        Log.d("MyLog", "message = ${userJson.message}")  // = success
        Log.d("MyLog", "user = ${userJson.user}")        // = null

        return userJson.user
    }
}

When it goes into the function fromJson(), userJson.message = "success" as expected. But the strange thing is that userJson.user is null, which should be User(username="Eric").

I am new to Moshi and Kotlin, and I have already stuck with this problem for about 10 hours. Please help me out. Thanks for any help.

========================================

The following is the entire code of MainActivity.kt (50 lines only):

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Custom Type Adapters for Moshi
        val userMoshi = Moshi.Builder().add(UserAdapter()).build()

        val retrofit = Retrofit.Builder()
                .baseUrl("https://dl.dropboxusercontent.com/")
                .addConverterFactory(MoshiConverterFactory.create(userMoshi))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()

        val accountService = retrofit.create(AccountService::class.java)

        accountService.signUpAnonymously()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { user ->
                    Log.d("MyLog", user.toString())
                }
    }
}


// ========== For Retrofit ==========
interface AccountService {

    @GET("u/17350105/test.json")
    fun signUpAnonymously() : Observable<User>

}


// ========== For Moshi ==========
class User(var username: String)

class UserJson(var message: String, var user: User)

class UserAdapter {

    @FromJson fun fromJson(userJson: UserJson) : User {
        Log.d("MyLog", "message = ${userJson.message}")        // = success
        Log.d("MyLog", "user = ${userJson.user}")              // = null

        return userJson.user
    }

}

The build.gradle is:

compile "io.reactivex.rxjava2:rxjava:2.0.0"
compile "io.reactivex.rxjava2:rxandroid:2.0.0"

compile "com.android.support:appcompat-v7:25.0.0"

compile "com.squareup.retrofit2:retrofit:2.1.0"
compile "com.squareup.retrofit2:converter-moshi:2.1.0"
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'

Thank you again.

iForests
  • 6,757
  • 10
  • 42
  • 75

2 Answers2

5

You can solve the problem by changing your code to do something like below.

Basically in your case when the UserAdapter is registered, it tells moshi that it can create a User only from UserJson object. Hence Moshi does not recognize the JSON object with keyword user.

By adding an indirection in form of User1 (please pardon the naming convention), the UserJson is created properly with User1 from JSON.

class User(var username: String)

class User1(var username: String) // I introduced this class
class UserJson(var message: String, var user: User1) // changed User to User1

class UserAdapter {
    @FromJson fun fromJson(userJson: UserJson): User {
        println("message = ${userJson.message}")        
        println("user = ${userJson.user}")              
        return User(userJson.user.username)
    }
}
Praveer Gupta
  • 3,940
  • 2
  • 19
  • 21
  • 1
    It works, thanks! I have an additional question: Is there any better way to achieve the same goal? (The only thing I need to do is to get the `user` field from the original JSON.) – iForests Nov 04 '16 at 18:07
5

If you just need the User object. There is a library called Moshi-Lazy-Adapters that provides a @Wrapped annotation, that allows specifying the path to the desired object. All you have to do is add the respective adapter to your Moshi instance and change the service code to:

interface AccountService {

  @GET("u/17350105/test.json")
  @Wrapped("user")
  fun signUpAnonymously() : Observable<User>
}

No need for any other custom adapter.

Omid Heshmatinia
  • 5,089
  • 2
  • 35
  • 50
Serj Lotutovici
  • 4,370
  • 5
  • 32
  • 41
  • Just saw some related issues in Moshi's GitHub page (#182 #176 #113). Thanks so much for your Moshi-Lazy-Adapters! – iForests Nov 05 '16 at 04:04
  • Just to complement the answer, you should define the Adapter like this ".addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().add(Wrapped.ADAPTER_FACTORY).build()))" in the retrofit builder. And if you need a deeper path "@Wrapped(path = ["a", "b", "c"])" – isanjosgon Dec 05 '17 at 17:41