Sleepwalker Sleepwalker - 13 days ago 5
Android Question

Android Retrofit 2 multiple converters (Gson & SimpleXML) error

In my project (i am using Dagger 2, Retrofit 2 & OkHTTP, RxAndroid) i have 2 different API calls. From one i receive JSON, from other - XML. I studied this link and added 2 converters that i need to my Retrofit.Builder():

@Provides
@Singleton
public Gson providesGson() {
return new GsonBuilder().create();
}

@Provides
@Singleton
public Retrofit providesRetrofit(@NonNull OkHttpClient okHttpClient, @NonNull Gson gson) {
return new Retrofit.Builder()
.baseUrl(ConstantsManager.BASE_URL)
.client(okHttpClient)
.addConverterFactory(SimpleXmlConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
}


But then, when i receive JSON (XML gets converted correctly), i get the following:

D/OkHttp: {"date":"20.11.2016","bank":"PB","baseCurrency":980,"baseCurrencyLit":"UAH","exchangeRate":[{"baseCurrency":"UAH","currency":"AUD","saleRateNB":19.4452740,"purchaseRateNB":19.4452740},{"baseCurrency":"UAH","currency":"CAD","saleRateNB":19.4047320,"purchaseRateNB":19.4047320},{"baseCurrency":"UAH","currency":"CZK","saleRateNB":1.0322170,"purchaseRateNB":1.0322170,"saleRate":1.0800000,"purchaseRate":0.9800000},{"baseCurrency":"UAH","currency":"DKK","saleRateNB":3.7519280,"purchaseRateNB":3.7519280},{"baseCurrency":"UAH","currency":"HUF","saleRateNB":0.0902556,"purchaseRateNB":0.0902556},{"baseCurrency":"UAH","currency":"ILS","saleRateNB":6.7524710,"purchaseRateNB":6.7524710,"saleRate":7.0000000,"purchaseRate":6.3000000},{"baseCurrency":"UAH","currency":"JPY","saleRateNB":0.2384005,"purchaseRateNB":0.2384005,"saleRate":0.2500000,"purchaseRate":0.2200000},{"baseCurrency":"UAH","currency":"LVL","saleRateNB":0.2384005,"purchaseRateNB":0.2384005},{"baseCurrency":"UAH","currency":"LTL","saleRateNB":0.2384005,"purchaseRateNB":0.2384005},{"baseCurrency":"UAH","currency":"NOK","saleRateNB":3.0724120,"purchaseRateNB":3.0724120,"saleRate":3.2000000,"purchaseRate":2.9000000},{"baseCurrency":"UAH","currency":"SKK","saleRateNB":3.0724120,"purchaseRateNB":3.0724120},{"baseCurrency":"UAH","currency":"SEK","saleRateNB":2.8384710,"purchaseRateNB":2.8384710},{"baseCurrency":"UAH","currency":"CHF","saleRateNB":26.0049080,"purchaseRateNB":26.0049080,"saleRate":27.5000000,"purchaseRate":25.0000000},{"baseCurrency":"UAH","currency":"RUB","saleRateNB":0.4013400,"purchaseRateNB":0.4013400,"saleRate":0.4200000,"purchaseRate":0.4000000},{"baseCurrency":"UAH","currency":"GBP","saleRateNB":32.4460750,"purchaseRateNB":32.4460750,"saleRate":34.0000000,"purchaseRate":31.0000000},{"baseCurrency":"UAH","currency":"USD","saleRateNB":26.0534380,"purchaseRateNB":26.0534380,"saleRate":27.0000000,"purchaseRate":26.6000000},{"baseCurrency":"UAH","currency":"BYR","saleRateNB":26.0534380,"purchaseRateNB":26.0534380},{"baseCurrency":"UAH","currency":"EUR","saleRateNB":27.9214700,"purchaseRateNB":27.9214700,"saleRate":28.6000000,"purchaseRate":28.2000000},{"baseCurrency":"UAH","currency":"GEL","saleRateNB":10.5921530,"purchaseRateNB":10.5921530},{"baseCurrency":"UAH","currency":"PLZ","saleRateNB":6.2818280,"purchaseRateNB":6.2818280,"saleRate":6.6000000,"purchaseRate":5.9000000}]}
D/OkHttp: <-- END HTTP (2377-byte body)
E/DateCurrencyService: Error while loading data occurred!
java.lang.RuntimeException: org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT {"date":"20.11.2...@1:2378 in java.io.InputStreamReader@2ec8965)
at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:44)
at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:23)
at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117)
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171)
at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
at rx.Subscriber.setProducer(Subscriber.java:209)
at rx.Subscriber.setProducer(Subscriber.java:205)
at rx.Subscriber.setProducer(Subscriber.java:205)
at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
at rx.Subscriber.setProducer(Subscriber.java:205)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
at rx.Observable.unsafeSubscribe(Observable.java:8666)
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)
Caused by: org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT {"date":"20.11.2...@1:2378 in java.io.InputStreamReader@2ec8965)
at org.kxml2.io.KXmlParser.next(KXmlParser.java:432)
at org.kxml2.io.KXmlParser.next(KXmlParser.java:313)
at org.simpleframework.xml.stream.PullReader.read(PullReader.java:105)
at org.simpleframework.xml.stream.PullReader.next(PullReader.java:89)
at org.simpleframework.xml.stream.NodeReader.readElement(NodeReader.java:111)
at org.simpleframework.xml.stream.NodeReader.readRoot(NodeReader.java:85)
at org.simpleframework.xml.stream.NodeBuilder.read(NodeBuilder.java:84)
at org.simpleframework.xml.stream.NodeBuilder.read(NodeBuilder.java:71)
at org.simpleframework.xml.core.Persister.read(Persister.java:562)
at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:36)
at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:23) 
at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117) 
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211) 
at retrofit2.OkHttpCall.execute(OkHttpCall.java:174) 
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171) 
at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80) 
at rx.Subscriber.setProducer(Subscriber.java:209) 
at rx.Subscriber.setProducer(Subscriber.java:205) 
at rx.Subscriber.setProducer(Subscriber.java:205) 
at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76) 
at rx.Subscriber.setProducer(Subscriber.java:205) 
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152) 
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138) 
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50) 
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) 
at rx.Observable.unsafeSubscribe(Observable.java:8666) 
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94) 
at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220) 
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) 
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428) 
at java.util.concurrent.FutureTask.run(FutureTask.java:237) 
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272) 
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 
at java.lang.Thread.run(Thread.java:761) 


As i understood what is going on there is that Simple XML Converter tries to convert the JSON response i get from second call (XML response from first call, as i said earlier, get converted correctly). What i need to do is to convert each response with appropriate converter (XML response with the Simple XML Converter and JSON response with the Gson Converter)

If i try to switch the order of adding converters i will get a similar error when receiving XML (because Gson converter tries to convert XML response, obviously):

D/OkHttp: <exchangerate><exchangerate ccy="EUR" ccy_name_ru="Евро " ccy_name_ua="Євро " ccy_name_en="Euro " base_ccy="UA" buy="27247312" unit="100.00000" date="2016.11.29" /><exchangerate ccy="RUR" ccy_name_ru="Российский рубль " ccy_name_ua="Росiйський рубль " ccy_name_en="Russian Rouble " base_ccy="UA" buy="39810" unit="10.00000" date="2016.11.29" /><exchangerate ccy="USD" ccy_name_ru="Доллар США " ccy_name_ua="Долар США " ccy_name_en="US Dollar " base_ccy="UA" buy="25724426" unit="100.00000" date="2016.11.29" /></exchangerate>
D/OkHttp: <-- END HTTP (799-byte body)
E/SyncService: Error while loading data occurred!
com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1559)
at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1401)
at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:593)
at com.google.gson.stream.JsonReader.peek(JsonReader.java:425)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:205)
at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:37)
at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:25)
at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117)
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211)
at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171)
at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
at rx.Subscriber.setProducer(Subscriber.java:209)
at rx.Subscriber.setProducer(Subscriber.java:205)
at rx.Subscriber.setProducer(Subscriber.java:205)
at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
at rx.Subscriber.setProducer(Subscriber.java:205)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152)
at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
at rx.Observable.unsafeSubscribe(Observable.java:8666)
at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)


What can i do to make each converter convert the appropriate response?

UPD: As was suggested by Marcin Jedynak i did the following:

1) Added custom Converter class:

public class RetrofitUniversalConverter extends Converter.Factory {

private final Converter.Factory xml;
private final Converter.Factory json;

@Inject
public RetrofitUniversalConverter(@NonNull Gson gson) {
xml = SimpleXmlConverterFactory.create();
json = GsonConverterFactory.create(gson);
}

@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

for (Annotation annotation : annotations) {

if (annotation.getClass() == Xml.class) {
return xml.responseBodyConverter(type, annotations, retrofit);
}

if (annotation.getClass() == Json.class) {
return json.responseBodyConverter(type, annotations, retrofit);
}

}

return null;
}
}


2) Rewrited my Retrofit module:

@Provides
@Singleton
public Gson providesGson() {
return new GsonBuilder().create();
}

@Provides
@Singleton
public Retrofit providesRetrofit(@NonNull OkHttpClient okHttpClient,
@NonNull RetrofitUniversalConverter converter) {
return new Retrofit.Builder()
.baseUrl(ConstantsManager.BASE_URL)
.client(okHttpClient)
.addConverterFactory(converter)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
}


3) Then, i added annotations in my API interface:

public interface PrivatbankApi {

@GET @Xml
Observable<CurrentRates> loadCurrentRates(@NonNull @Url String url);

@GET("exchange_rates") @Json
Observable<DateRates> loadDateRates(@NonNull @Query("json") Boolean json, @NonNull @Query("date") String date);

}


But then i get exception:

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.vedmedenko.exchangerates, PID: 5432
java.lang.RuntimeException: Unable to start service com.vedmedenko.exchangerates.core.services.SyncService@68cf714 with Intent { flg=0x4 cmp=com.vedmedenko.exchangerates/.core.services.SyncService (has extras) }: java.lang.IllegalArgumentException: Unable to create converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates
for method PrivatbankApi.loadCurrentRates
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3343)
at android.app.ActivityThread.-wrap21(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1582)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: java.lang.IllegalArgumentException: Unable to create converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates
for method PrivatbankApi.loadCurrentRates
at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:720)
at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:706)
at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:167)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:166)
at retrofit2.Retrofit$1.invoke(Retrofit.java:145)
at java.lang.reflect.Proxy.invoke(Proxy.java:813)
at $Proxy0.loadCurrentRates(Unknown Source)
at com.vedmedenko.exchangerates.core.DataManager.loadCurrentRates(DataManager.java:29)
at com.vedmedenko.exchangerates.core.services.SyncService.onStartCommand(SyncService.java:81)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3326)
... 8 more
Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates.
Tried:
* retrofit2.BuiltInConverters
* com.vedmedenko.exchangerates.core.rest.converters.RetrofitUniversalConverter
at retrofit2.Retrofit.nextResponseBodyConverter(Retrofit.java:346)
at retrofit2.Retrofit.responseBodyConverter(Retrofit.java:308)
at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:704)
... 16 more

Answer

Check this presentation by Jake Wharton, where he solves exactly the problem you describe (and presents many other useful tricks).

In short he suggests creating annotations representing your expected data format (for example Json and Xml in your case) and annotate your API calls accordingly. Then you define your custom ConverterFactory where you delegate to either GsonConverterFactory or SimpleXmlConverterFactory depending on the annotation encountered.

However, you need add two things to the Jake's solution.

First do not forget to annotate your annotations with @Retention(RetentionPolicy.RUNTIME) otherwise they won't be kept during the runtime:

@Retention(RetentionPolicy.RUNTIME)
public @interface Json {}

Second, the annotations you receive in your responseBodyConverter method are not actually your annotations. They are proxies created by the system to your annotations. Therefore you need to replace the conditions:

annotation.getClass() == Json.class
annotation.getClass() == Xml.class

with:

annotation.annotationType() == Json.class
annotation.annotationType() == Xml.class