Keywords: Retrofit | HTTP Header Parameters | JSON Parsing Errors
Abstract: This article provides an in-depth exploration of correctly adding API keys as HTTP header parameters in Retrofit and analyzes common JSON parsing errors. By comparing implementations between HttpURLConnection and Retrofit, it explains the usage of @Header and @Headers annotations, and how to globally add header parameters using OkHttp interceptors. The article focuses on analyzing the root cause of the "Expected a string but was BEGIN_OBJECT" error and provides solutions using POJO classes instead of String types to ensure successful API execution.
Multiple Methods for Adding HTTP Header Parameters in Retrofit
In Android development, Retrofit, as a mainstream HTTP client library, offers various flexible ways to add HTTP header parameters. Compared to traditional HttpURLConnection, Retrofit makes header parameter management more concise and maintainable through annotations and interceptor mechanisms.
Dynamically Adding Header Parameters Using @Header Annotation
When header parameter values need to be passed dynamically for each API call, the @Header annotation can be used. This method is suitable for authentication information like API keys that may change:
@GET("api/v2.1/search")
Call<String> getRestaurantsBySearch(
@Query("entity_id") String entity_id,
@Query("entity_type") String entity_type,
@Query("q") String query,
@Header("user-key") String userkey
);
When calling, pass the API key as a parameter:
Call<String> call = endpoint.getRestaurantsBySearch(
"3", "city", "cafes", "9900a9720d31dfd5fdb4352700c"
);
Statically Adding Header Parameters Using @Headers Annotation
For header parameters that remain constant, the @Headers annotation can be used to declare them directly on the interface method:
@Headers("user-key: 9900a9720d31dfd5fdb4352700c")
@GET("api/v2.1/search")
Call<String> getRestaurantsBySearch(
@Query("entity_id") String entity_id,
@Query("entity_type") String entity_type,
@Query("q") String query
);
This method simplifies the calling code, eliminating the need to pass header parameter values each time. If multiple header parameters need to be added, an array format can be used:
@Headers({
"Accept: application/json",
"user-key: 9900a9720d31dfd5fdb4352700c",
"Cache-Control: max-age=640000"
})
Globally Adding Header Parameters Using OkHttp Interceptors
When the same header parameters need to be added to all API calls, this can be achieved through OkHttp interceptors:
OkHttpClient httpClient = new OkHttpClient();
httpClient.networkInterceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder requestBuilder = chain.request().newBuilder();
requestBuilder.header("user-key", "9900a9720d31dfd5fdb4352700c");
return chain.proceed(requestBuilder.build());
}
});
Then pass the configured OkHttpClient instance to the Retrofit builder:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://developers.zomato.com/")
.client(httpClient)
.addConverterFactory(ScalarsConverterFactory.create())
.build();
Root Causes and Solutions for JSON Parsing Errors
During API calls, the common error java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 2 path $ typically stems from response data parsing issues.
Error Cause Analysis
This error indicates that Retrofit expects to receive a string-type response but actually receives a JSON object. When the API returns structured JSON data (such as objects or arrays), using Call<String> causes parsing failure because Retrofit cannot directly convert JSON objects to strings.
Correct Data Binding Using POJO Classes
The correct solution to this problem is to define POJO classes that match the API response structure and use Call<Data> instead of Call<String>:
public class RestaurantResponse {
private List<Restaurant> restaurants;
private int results_found;
private int results_start;
private int results_shown;
// Getters and setters
}
public class Restaurant {
private String id;
private String name;
private String cuisines;
private String thumb;
// Getters and setters
}
Then use the correct response type in the Retrofit interface:
@Headers("user-key: 9900a9720d31dfd5fdb4352700c")
@GET("api/v2.1/search")
Call<RestaurantResponse> getRestaurantsBySearch(
@Query("entity_id") String entity_id,
@Query("entity_type") String entity_type,
@Query("q") String query
);
Configuring the Correct Converter
Ensure that the Gson converter is added to the Retrofit builder:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://developers.zomato.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
And add to Gradle dependencies:
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
Complete Implementation Example and Best Practices
Combining header parameter addition and correct response handling, a complete implementation example is as follows:
// 1. Define data models
public class RestaurantSearchResult {
@SerializedName("restaurants")
private List<Restaurant> restaurants;
// Other fields and getter/setter
}
// 2. Define Retrofit interface
public interface ZomatoApiService {
@Headers("user-key: YOUR_API_KEY_HERE")
@GET("api/v2.1/search")
Call<RestaurantSearchResult> searchRestaurants(
@Query("entity_id") String entityId,
@Query("entity_type") String entityType,
@Query("q") String query
);
}
// 3. Configure Retrofit client
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("Accept", "application/json")
.method(original.method(), original.body())
.build();
return chain.proceed(request);
}
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://developers.zomato.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build();
// 4. Create service instance and call API
ZomatoApiService service = retrofit.create(ZomatoApiService.class);
Call<RestaurantSearchResult> call = service.searchRestaurants("3", "city", "mumbai");
call.enqueue(new Callback<RestaurantSearchResult>() {
@Override
public void onResponse(Call<RestaurantSearchResult> call, Response<RestaurantSearchResult> response) {
if (response.isSuccessful()) {
RestaurantSearchResult result = response.body();
// Process response data
} else {
// Handle HTTP errors
}
}
@Override
public void onFailure(Call<RestaurantSearchResult> call, Throwable t) {
// Handle network or parsing errors
}
});
Summary and Recommendations
Correctly handling API keys and other header parameters in Retrofit requires considering multiple factors:
- Choose Appropriate Header Parameter Addition Methods: Select
@Header,@Headers, or interceptors based on header parameter dynamics - Correctly Define Response Data Types: Avoid using
Call<String>for JSON object responses; use matching POJO classes instead - Configure Correct Converters: Choose GsonConverterFactory or ScalarsConverterFactory based on response data types
- Verify Base URL and Endpoint Paths: Ensure consistency with API documentation to avoid path concatenation errors
- Implement Complete Error Handling: Handle successful responses, HTTP errors, and network exceptions separately in callbacks
By following these best practices, the stability and maintainability of Retrofit API calls can be ensured while avoiding common parsing errors and authentication issues.